[tab] crowd-pilot-extension/.crowd-pilot-preferences.jsonl (jsonl)
{"timestamp":1765386760168,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>

</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1,1p'
```
"},{"role":"user","content":"<stdout>

</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/.crowd-pilot-preferences.jsonl
```
"},{"role":"user","content":"<stdout>
     1	{\"timestamp\":1765386740877,\"context\":[{\"role\":\"system\",\"content\":\"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
     2	Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.
     3	
     4	=== CONVERSATION FORMAT ===
     5	The conversation history alternates between:
     6	- Assistant messages: bash commands in fenced code blocks
     7	- User messages: command output wrapped in <stdout>...</stdout> tags
     8	
     9	File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
    10	     1\	first line
    11	     2\	second line
    12	
    13	File content is typically shown in viewports of ~20 lines around the area of interest.
    14	
    15	=== RESPONSE FORMAT ===
    16	Your response must contain exactly ONE bash code block with one command or two commands connected with &&.
    17	
    18	<format_example>
    19	```bash
    20	your_command_here
    21	```
    22	</format_example>
    23	
    24	Failure to follow these rules will cause your response to be rejected.
    25	
    26	=== EDIT COMMAND FORMAT (IMPORTANT) ===
    27	When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
    28	and you MUST NOT use substitution commands like \\\"Ns/old/new/g\\\".
    29	
    30	Assume all line numbers are 1-based and paths are absolute.
    31	Allowed edit encodings (choose exactly one per response):
    32	
    33	1) Replace a contiguous block of lines:
    34	   sed -i 'START,ENDc\\\\
    35	NEW_LINE_1
    36	NEW_LINE_2
    37	...
    38	' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'
    39	
    40	2) Delete a contiguous block of lines:
    41	   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'
    42	
    43	3) Insert new lines BEFORE a given line:
    44	   sed -i 'STARTi\\\\
    45	NEW_LINE_1
    46	NEW_LINE_2
    47	...
    48	' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'
    49	
    50	4) Append new lines at the END of the file:
    51	   sed -i '$a\\\\
    52	NEW_LINE_1
    53	NEW_LINE_2
    54	...
    55	' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'
    56	
    57	Where VSTART and VEND specify a small viewport around the edited region.
    58	
    59	Do NOT emit commands like \\\"3s/print/print()/g\\\" or any other \\\"s/old/new/\\\" style sed substitution; instead,
    60	always rewrite the affected lines using one of the line-based forms above.
    61	
    62	When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands.\"},{\"role\":\"assistant\",\"content\":\"```bash
    63	cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
    64	```
    65	\"},{\"role\":\"user\",\"content\":\"<stdout>
    66	
    67	</stdout>\"},{\"role\":\"assistant\",\"content\":\"```bash
    68	cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1,1p'
    69	```
    70	\"},{\"role\":\"user\",\"content\":\"<stdout>
    71	
    72	</stdout>\"},{\"role\":\"assistant\",\"content\":\"```bash
    73	cat -n /home/franz.srambical/crowd-pilot-extension/.crowd-pilot-preferences.jsonl
    74	```
    75	\"},{\"role\":\"user\",\"content\":\"<stdout>
    76	     1\	{\\\"timestamp\\\":1765386485465,\\\"context\\\":[{\\\"role\\\":\\\"system\\\",\\\"content\\\":\\\"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
    77	     2\	Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.
    78	     3\	
    79	     4\	=== CONVERSATION FORMAT ===
    80	     5\	The conversation history alternates between:
    81	     6\	- Assistant messages: bash commands in fenced code blocks
    82	     7\	- User messages: command output wrapped in <stdout>...</stdout> tags
    83	     8\	
    84	     9\	File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
    85	    10\	     1\\\	first line
    86	    11\	     2\\\	second line
    87	    12\	
    88	    13\	File content is typically shown in viewports of ~20 lines around the area of interest.
    89	    14\	
    90	    15\	=== RESPONSE FORMAT ===
    91	    16\	Your response must contain exactly ONE bash code block with one command or two commands connected with &&.
    92	    17\	
    93	    18\	<format_example>
    94	    19\	```bash
    95	    20\	your_command_here
    96	    21\	```
    97	    22\	</format_example>
    98	    23\	
    99	    24\	Failure to follow these rules will cause your response to be rejected.
   100	    25\	
   101	    26\	=== EDIT COMMAND FORMAT (IMPORTANT) ===
   102	    27\	When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
   103	    28\	and you MUST NOT use substitution commands like \\\\\\\"Ns/old/new/g\\\\\\\".
   104	    29\	
   105	    30\	Assume all line numbers are 1-based and paths are absolute.
   106	    31\	Allowed edit encodings (choose exactly one per response):
   107	    32\	
   108	    33\	1) Replace a contiguous block of lines:
   109	    34\	   sed -i 'START,ENDc\\\\\\\\
   110	    35\	NEW_LINE_1
   111	    36\	NEW_LINE_2
   112	    37\	...
   113	    38\	' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'
   114	    39\	
   115	    40\	2) Delete a contiguous block of lines:
   116	    41\	   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'
   117	    42\	
   118	    43\	3) Insert new lines BEFORE a given line:
   119	    44\	   sed -i 'STARTi\\\\\\\\
   120	    45\	NEW_LINE_1
   121	    46\	NEW_LINE_2
   122	    47\	...
   123	    48\	' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'
   124	    49\	
   125	    50\	4) Append new lines at the END of the file:
   126	    51\	   sed -i '$a\\\\\\\\
   127	    52\	NEW_LINE_1
   128	    53\	NEW_LINE_2
   129	    54\	...
   130	    55\	' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'
   131	    56\	
   132	    57\	Where VSTART and VEND specify a small viewport around the edited region.
   133	    58\	
   134	    59\	Do NOT emit commands like \\\\\\\"3s/print/print()/g\\\\\\\" or any other \\\\\\\"s/old/new/\\\\\\\" style sed substitution; instead,
   135	    60\	always rewrite the affected lines using one of the line-based forms above.
   136	    61\	
   137	    62\	When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands.\\\"},{\\\"role\\\":\\\"assistant\\\",\\\"content\\\":\\\"```bash
   138	    63\	cat -n Untitled-2
   139	    64\	```
   140	    65\	\\\"},{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"<stdout>
   141	    66\	
   142	    67\	</stdout>\\\"},{\\\"role\\\":\\\"assistant\\\",\\\"content\\\":\\\"```bash
   143	    68\	sed -i '1,1c\\\\\\\\
   144	    69\	\\\\\\\"\\\\\\\"' Untitled-2 && cat -n Untitled-2 | sed -n '1,1p'
   145	    70\	```
   146	    71\	\\\"},{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"<stdout>
   147	    72\	     1\\\	\\\\\\\"\\\\\\\"
   148	    73\	</stdout>\\\"},{\\\"role\\\":\\\"assistant\\\",\\\"content\\\":\\\"```bash
   149	    74\	sed -i '1,1c\\\\\\\\
   150	    75\	\\\\\\\"\\\\\\\"\\\\\\\"' Untitled-2 && cat -n Untitled-2 | sed -n '1,1p'
   151	    76\	```
   152	    77\	\\\"},{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"<stdout>
   153	    78\	     1\\\	\\\\\\\"\\\\\\\"\\\\\\\"
   154	    79\	</stdout>\\\"},{\\\"role\\\":\\\"assistant\\\",\\\"content\\\":\\\"```bash
   155	    80\	sed -i '1,1c\\\\\\\\
   156	    81\	\\\\\\\"\\\\\\\"\\\\\\\"fi' Untitled-2 && cat -n Untitled-2 | sed -n '1,1p'
   157	    82\	```
   158	    83\	\\\"},{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"<stdout>
   159	    84\	     1\\\	\\\\\\\"\\\\\\\"\\\\\\\"fi
   160	    85\	</stdout>\\\"},{\\\"role\\\":\\\"assistant\\\",\\\"content\\\":\\\"```bash
   161	    86\	sed -i '1,1c\\\\\\\\
   162	    87\	\\\\\\\"\\\\\\\"\\\\\\\"fiv' Untitled-2 && cat -n Untitled-2 | sed -n '1,1p'
   163	    88\	```
   164	    89\	\\\"},{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"<stdout>
   165	    90\	     1\\\	\\\\\\\"\\\\\\\"\\\\\\\"fiv
   166	    91\	</stdout>\\\"},{\\\"role\\\":\\\"assistant\\\",\\\"content\\\":\\\"```bash
   167	    92\	sed -i '1,1c\\\\\\\\
   168	    93\	\\\\\\\"\\\\\\\"\\\\\\\"fibon' Untitled-2 && cat -n Untitled-"},{"role":"assistant","content":"```bash
sed -i '2680,2680c\\
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      {\"timestamp\":1765386748465,\"context\":[{\"role\":\"system\",\"content\":\"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1\	first line
     2\	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \\\"Ns/old/new/g\\\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i '\"'\"'START,ENDc\\\\
NEW_LINE_1
NEW_LINE_2
...
'\"'\"' /abs/path/to/file && cat -n /abs/path/to/file | sed -n '\"'\"'VSTART,VENDp'\"'\"'

2) Delete a contiguous block of lines:
   sed -i '\"'\"'START,ENDd'\"'\"' /abs/path/to/file && cat -n /abs/path/to/file | sed -n '\"'\"'VSTART,VENDp'\"'\"'

3) Insert new lines BEFORE a given line:
   sed -i '\"'\"'STARTi\\\\
NEW_LINE_1
NEW_LINE_2
...
'\"'\"' /abs/path/to/file && cat -n /abs/path/to/file | sed -n '\"'\"'VSTART,VENDp'\"'\"'

4) Append new lines at the END of the file:
   sed -i '\"'\"'$a\\\\
NEW_LINE_1
NEW_LINE_2
...
'\"'\"' /abs/path/to/file && cat -n /abs/path/to/file | sed -n '\"'\"'VSTART,VENDp'\"'\"'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \\\"3s/print/print()/g\\\" or any other \\\"s/old/new/\\\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands.\"},{\"role\":\"assistant\",\"content\":\"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
\"},{\"role\":\"user\",\"content\":\"<stdout>

</stdout>\"},{\"role\":\"assistant\",\"content\":\"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '\"'\"'1,1p'\"'\"'
```
\"},{\"role\":\"user\",\"content\":\"<stdout>

</stdout>\"}],\"completion\":{\"rawModelOutput\":\"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '\"'\"'2,2p'\"'\"'
```\",\"parsedAction\":{\"kind\":\"setSelections\",\"selections\":[{\"start\":[1,29],\"end\":[1,29]}]},\"avgLogprob\":-0.000023921684868039643},\"outcome\":\"ignored\",\"outcomeTimestamp\":1765386750046,\"modelName\":\"qwen/qwen3-8b\"}
' /home/franz.srambical/crowd-pilot-extension/.crowd-pilot-preferences.jsonl && cat -n /home/franz.srambical/crowd-pilot-extension/.crowd-pilot-preferences.jsonl | sed -n '2707,2727p'
```
"},{"role":"user","content":"<stdout>
  2707	and you MUST NOT use substitution commands like \\\"Ns/old/new/g\\\".
  2708	
  2709	Assume all line numbers are 1-based and paths are absolute.
  2710	Allowed edit encodings (choose exactly one per response):
  2711	
  2712	1) Replace a contiguous block of lines:
  2713	   sed -i 'START,ENDc\\\\
  2714	NEW_LINE_1
  2715	NEW_LINE_2
  2716	...
  2717	' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'
  2718	
  2719	2) Delete a contiguous block of lines:
  2720	   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'
  2721	
  2722	3) Insert new lines BEFORE a given line:
  2723	   sed -i 'STARTi\\\\
  2724	NEW_LINE_1
  2725	NEW_LINE_2
  2726	...
  2727	' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'
</stdout>"},{"role":"assistant","content":"```bash
sed -i '1,2754c\\
' /home/franz.srambical/crowd-pilot-extension/.crowd-pilot-preferences.jsonl && cat -n /home/franz.srambical/crowd-pilot-extension/.crowd-pilot-preferences.jsonl | sed -n '1,1p'
```
"},{"role":"user","content":"<stdout>

</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/.crowd-pilot-preferences.jsonl | sed -n '1,1p'
```
"},{"role":"user","content":"<stdout>

</stdout>"},{"role":"assistant","content":"```bash
sed -i '1,1c\\
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          "},{"role":"user","content":"<stdout>
   291	...
   292	'\\\"'\\\"' /abs/path/to/file && cat -n /abs/path/to/file | sed -n '\\\"'\\\"'VSTART,VENDp'\\\"'\\\"'
   293	
   294	4) Append new lines at the END of the file:
   295	   sed -i '\\\"'\\\"'$a\\\\\\\\
   296	NEW_LINE_1
   297	NEW_LINE_2
   298	...
   299	'\\\"'\\\"' /abs/path/to/file && cat -n /abs/path/to/file | sed -n '\\\"'\\\"'VSTART,VENDp'\\\"'\\\"'
   300	
   301	Where VSTART and VEND specify a small viewport around the edited region.
   302	
   303	Do NOT emit commands like \\\\\\\"3s/print/print()/g\\\\\\\" or any other \\\\\\\"s/old/new/\\\\\\\" style sed substitution; instead,
   304	always rewrite the affected lines using one of the line-based forms above.
   305	
   306	When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands.\\\"},{\\\"role\\\":\\\"assistant\\\",\\\"content\\\":\\\"```bash
   307	cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
   308	```
   309	\\\"},{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"<stdout>
   310	
   311	</stdout>\\\"},{\\\"role\\\":\\\"assistant\\\",\\\"content\\\":\\\"```bash
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```","parsedAction":{"kind":"terminalSendText","text":"cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'"},"avgLogprob":-0.004363592286654594},"outcome":"ignored","outcomeTimestamp":1765386767432,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765386767433,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '739,759p'
```","parsedAction":{"kind":"setSelections","selections":[{"start":[748,75],"end":[748,75]}]},"avgLogprob":-0.0010372616628959719},"outcome":"ignored","outcomeTimestamp":1765386819093,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765386819093,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '819,839p'
```
```bash
```","parsedAction":{"kind":"setSelections","selections":[{"start":[828,3],"end":[828,3]}]},"avgLogprob":-0.01872435187173475},"outcome":"ignored","outcomeTimestamp":1765386820336,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765386820337,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '736,756p'
```","parsedAction":{"kind":"setSelections","selections":[{"start":[745,21],"end":[745,21]}]},"avgLogprob":-0.005381506490594745},"outcome":"ignored","outcomeTimestamp":1765386821920,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765386821920,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '739,759p'
```","parsedAction":{"kind":"setSelections","selections":[{"start":[748,75],"end":[748,75]}]},"avgLogprob":-0.018806323074024432},"outcome":"ignored","outcomeTimestamp":1765386825579,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765386825580,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```","parsedAction":{"kind":"setSelections","selections":[{"start":[778,2],"end":[778,2]}]},"avgLogprob":-0.000004187530889865343},"outcome":"ignored","outcomeTimestamp":1765386827338,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765386827338,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```","parsedAction":{"kind":"setSelections","selections":[{"start":[727,0],"end":[727,0]}]},"avgLogprob":-0.00006124709380408432},"outcome":"ignored","outcomeTimestamp":1765386829526,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765386829526,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```","parsedAction":{"kind":"setSelections","selections":[{"start":[778,2],"end":[778,2]}]},"avgLogprob":-0.0000027806357152826424},"outcome":"ignored","outcomeTimestamp":1765386837415,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765386837416,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '718,738p'
```
"},{"role":"user","content":"<stdout>
   718	${viewportOutput}
   719	</stdout>`,
   720					});
   721				}
   722			}
   723		}
   724	
   725		handleContentEvent(filePath: string, offset: number, length: number, newText: string): void {
   726			this.flushTerminalOutputBuffer();
   727	
   728			const before = this.fileStates.get(filePath) ?? '';
   729			const newTextStr = newText ?? '';
   730	
   731			// Approximate current edit region in line space
   732			const startLineCurrent = before.slice(0, offset).split('
   733	').length;
   734			const deletedContent = before.slice(offset, offset + length);
   735			const linesAdded = (newTextStr.match(/
   736	/g) || []).length;
   737			const linesDeleted = (deletedContent.match(/
   738	/g) || []).length;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '769,789p'
```
"},{"role":"user","content":"<stdout>
   769			this.fileStates.set(filePath, after);
   770		}
   771	
   772		handleSelectionEvent(filePath: string, offset: number): void {
   773			if (this.pendingEditsBefore.get(filePath) !== null && this.pendingEditsBefore.get(filePath) !== undefined) {
   774				return;
   775			}
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '811,831p'
```
"},{"role":"user","content":"<stdout>
   811	${viewportOutput}
   812	</stdout>`,
   813				});
   814			}
   815		}
   816	
   817		handleTerminalCommandEvent(command: string): void {
   818			this.flushAllPendingEdits();
   819			this.flushTerminalOutputBuffer();
   820	
   821			const commandStr = command.replace(/\\
   822	/g, '
   823	').replace(/\\
/g, '
');
   824			this.appendMessage({
   825				from: 'Assistant',
   826				value: fencedBlock('bash', cleanText(commandStr)),
   827			});
   828		}
   829	
   830		handleTerminalOutputEvent(output: string): void {
   831			const rawOutput = output.replace(/\\
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '811,831p'
```","parsedAction":{"kind":"setSelections","selections":[{"start":[820,1],"end":[820,1]}]},"avgLogprob":-0.00004179759819322726},"outcome":"rejected","outcomeTimestamp":1765386862693,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765441411183,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n extension-output-pdoom-org.crowd-code-#1-crowd-code
```
"},{"role":"user","content":"<stdout>
     1	9:23:30 AM [info] Activating crowd-code
     2	9:23:30 AM [info] Recording started
     3	9:23:30 AM [info] Initializing git provider using file system watchers...
     4	9:23:30 AM [info] Git repository found
     5	9:23:30 AM [info] Git provider initialized successfully
     6	9:23:30 AM [info] Initial git state: [object Object]
     7	
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```","parsedAction":{"kind":"terminalSendText","text":"cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts"},"avgLogprob":-0.0006643238585750337},"outcome":"ignored","outcomeTimestamp":1765441414623,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765441414623,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n extension-output-pdoom-org.crowd-code-#1-crowd-code
```
"},{"role":"user","content":"<stdout>
     1	9:23:30 AM [info] Activating crowd-code
     2	9:23:30 AM [info] Recording started
     3	9:23:30 AM [info] Initializing git provider using file system watchers...
     4	9:23:30 AM [info] Git repository found
     5	9:23:30 AM [info] Git provider initialized successfully
     6	9:23:30 AM [info] Initial git state: [object Object]
     7	
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1,11p'
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '12,21p'
```","parsedAction":{"kind":"setSelections","selections":[{"start":[15,3],"end":[15,3]}]},"avgLogprob":-0.0024565155497414614},"outcome":"ignored","outcomeTimestamp":1765441420267,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765441420267,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n extension-output-pdoom-org.crowd-code-#1-crowd-code
```
"},{"role":"user","content":"<stdout>
     1	9:23:30 AM [info] Activating crowd-code
     2	9:23:30 AM [info] Recording started
     3	9:23:30 AM [info] Initializing git provider using file system watchers...
     4	9:23:30 AM [info] Git repository found
     5	9:23:30 AM [info] Git provider initialized successfully
     6	9:23:30 AM [info] Initial git state: [object Object]
     7	
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1,11p'
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '776,796p'
```
"},{"role":"user","content":"<stdout>
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   790				this.perFileViewport.set(filePath, vp);
   791				shouldEmit = true;
   792			} else {
   793				if (targetLine < vp.start || targetLine > vp.end) {
   794					vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   795					this.perFileViewport.set(filePath, vp);
   796					shouldEmit = true;
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '130,150p'
```
```bash
```","parsedAction":{"kind":"setSelections","selections":[{"start":[139,80],"end":[139,80]}]},"avgLogprob":-0.04330017419027506},"outcome":"ignored","outcomeTimestamp":1765441421177,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765441421178,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n extension-output-pdoom-org.crowd-code-#1-crowd-code
```
"},{"role":"user","content":"<stdout>
     1	9:23:30 AM [info] Activating crowd-code
     2	9:23:30 AM [info] Recording started
     3	9:23:30 AM [info] Initializing git provider using file system watchers...
     4	9:23:30 AM [info] Git repository found
     5	9:23:30 AM [info] Git provider initialized successfully
     6	9:23:30 AM [info] Initial git state: [object Object]
     7	
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1,11p'
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '776,796p'
```
"},{"role":"user","content":"<stdout>
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   790				this.perFileViewport.set(filePath, vp);
   791				shouldEmit = true;
   792			} else {
   793				if (targetLine < vp.start || targetLine > vp.end) {
   794					vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   795					this.perFileViewport.set(filePath, vp);
   796					shouldEmit = true;
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1400,1420p'
```","parsedAction":{"kind":"setSelections","selections":[{"start":[1409,15],"end":[1409,15]}]},"avgLogprob":-0.025766639970266323},"outcome":"accepted","outcomeTimestamp":1765441431798,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765441431946,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n extension-output-pdoom-org.crowd-code-#1-crowd-code
```
"},{"role":"user","content":"<stdout>
     1	9:23:30 AM [info] Activating crowd-code
     2	9:23:30 AM [info] Recording started
     3	9:23:30 AM [info] Initializing git provider using file system watchers...
     4	9:23:30 AM [info] Git repository found
     5	9:23:30 AM [info] Git provider initialized successfully
     6	9:23:30 AM [info] Initial git state: [object Object]
     7	
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1,11p'
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '776,796p'
```
"},{"role":"user","content":"<stdout>
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   790				this.perFileViewport.set(filePath, vp);
   791				shouldEmit = true;
   792			} else {
   793				if (targetLine < vp.start || targetLine > vp.end) {
   794					vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   795					this.perFileViewport.set(filePath, vp);
   796					shouldEmit = true;
</stdout>"},{"role":"assistant","content":"```bash
squeue
```
"},{"role":"user","content":"<stdout>
  JOBID       USER PARTITION NODES  CPUS ST         SUBMIT_TIME          START_TIME     TIME TIME_LIMIT NODELIST(REASON)
  36260   xiao.liu interacti     1   128  R 2025-12-11T07:00:12 2025-12-11T07:00:12  2:23:32   23:59:00 hai006
  36257   xiao.liu interacti     1   128  R 2025-12-11T06:50:04 2025-12-11T06:50:04  2:33:40   23:59:00 hai005
  36249 franz.sram interacti     1    20  R 2025-12-10T16:52:17 2025-12-10T16:52:17 16:31:27 1-00:00:00 hai004
  36256 nishant.ku  standard     3   624  R 2025-12-11T06:46:30 2025-12-11T06:46:38  2:37:06 1-00:00:00 hai[001-003]
  36157   xiao.liu  standard     1   128  R 2025-12-10T09:29:57 2025-12-10T15:15:29 18:08:15   23:59:00 hai007
</stdout>"}],"completion":{"rawModelOutput":"```bash
scontrol show job 36249
```","parsedAction":{"kind":"terminalSendText","text":"scontrol show job 36249"},"avgLogprob":-0.004239264526447073},"outcome":"ignored","outcomeTimestamp":1765441432346,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765441432346,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n extension-output-pdoom-org.crowd-code-#1-crowd-code
```
"},{"role":"user","content":"<stdout>
     1	9:23:30 AM [info] Activating crowd-code
     2	9:23:30 AM [info] Recording started
     3	9:23:30 AM [info] Initializing git provider using file system watchers...
     4	9:23:30 AM [info] Git repository found
     5	9:23:30 AM [info] Git provider initialized successfully
     6	9:23:30 AM [info] Initial git state: [object Object]
     7	
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1,11p'
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '776,796p'
```
"},{"role":"user","content":"<stdout>
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   790				this.perFileViewport.set(filePath, vp);
   791				shouldEmit = true;
   792			} else {
   793				if (targetLine < vp.start || targetLine > vp.end) {
   794					vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   795					this.perFileViewport.set(filePath, vp);
   796					shouldEmit = true;
</stdout>"},{"role":"assistant","content":"```bash
squeue
```
"},{"role":"user","content":"<stdout>
  JOBID       USER PARTITION NODES  CPUS ST         SUBMIT_TIME          START_TIME     TIME TIME_LIMIT NODELIST(REASON)
  36260   xiao.liu interacti     1   128  R 2025-12-11T07:00:12 2025-12-11T07:00:12  2:23:32   23:59:00 hai006
  36257   xiao.liu interacti     1   128  R 2025-12-11T06:50:04 2025-12-11T06:50:04  2:33:40   23:59:00 hai005
  36249 franz.sram interacti     1    20  R 2025-12-10T16:52:17 2025-12-10T16:52:17 16:31:27 1-00:00:00 hai004
  36256 nishant.ku  standard     3   624  R 2025-12-11T06:46:30 2025-12-11T06:46:38  2:37:06 1-00:00:00 hai[001-003]
  36157   xiao.liu  standard     1   128  R 2025-12-10T09:29:57 2025-12-10T15:15:29 18:08:15   23:59:00 hai007
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1463,1483p'
```
"},{"role":"user","content":"<stdout>
  1463	
  1464		pendingPredictions.push({ id, timer });
  1465	
  1466		if (pendingPredictions.length > 2) {
  1467			const oldest = pendingPredictions.shift();
  1468			if (oldest) {
  1469				cancelledPredictionIds.add(oldest.id);
  1470				clearTimeout(oldest.timer);
  1471			}
  1472		}
  1473	}
  1474	
  1475	async function autoShowNextAction(): Promise<void> {
  1476		if (suppressAutoPreview) { return; }
  1477		const editor = vscode.window.activeTextEditor;
  1478		if (!editor) { return; }
  1479		try {
  1480			currentAbortController?.abort();
  1481			const controller = new AbortController();
  1482			currentAbortController = controller;
  1483			const requestId = ++latestRequestId;
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1523,1543p'
```","parsedAction":{"kind":"setSelections","selections":[{"start":[1532,22],"end":[1532,22]}]},"avgLogprob":-0.12043477658707638},"outcome":"accepted","outcomeTimestamp":1765441433523,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765441434079,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n extension-output-pdoom-org.crowd-code-#1-crowd-code
```
"},{"role":"user","content":"<stdout>
     1	9:23:30 AM [info] Activating crowd-code
     2	9:23:30 AM [info] Recording started
     3	9:23:30 AM [info] Initializing git provider using file system watchers...
     4	9:23:30 AM [info] Git repository found
     5	9:23:30 AM [info] Git provider initialized successfully
     6	9:23:30 AM [info] Initial git state: [object Object]
     7	
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1,11p'
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '776,796p'
```
"},{"role":"user","content":"<stdout>
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   790				this.perFileViewport.set(filePath, vp);
   791				shouldEmit = true;
   792			} else {
   793				if (targetLine < vp.start || targetLine > vp.end) {
   794					vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   795					this.perFileViewport.set(filePath, vp);
   796					shouldEmit = true;
</stdout>"},{"role":"assistant","content":"```bash
squeue
```
"},{"role":"user","content":"<stdout>
  JOBID       USER PARTITION NODES  CPUS ST         SUBMIT_TIME          START_TIME     TIME TIME_LIMIT NODELIST(REASON)
  36260   xiao.liu interacti     1   128  R 2025-12-11T07:00:12 2025-12-11T07:00:12  2:23:32   23:59:00 hai006
  36257   xiao.liu interacti     1   128  R 2025-12-11T06:50:04 2025-12-11T06:50:04  2:33:40   23:59:00 hai005
  36249 franz.sram interacti     1    20  R 2025-12-10T16:52:17 2025-12-10T16:52:17 16:31:27 1-00:00:00 hai004
  36256 nishant.ku  standard     3   624  R 2025-12-11T06:46:30 2025-12-11T06:46:38  2:37:06 1-00:00:00 hai[001-003]
  36157   xiao.liu  standard     1   128  R 2025-12-10T09:29:57 2025-12-10T15:15:29 18:08:15   23:59:00 hai007
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1463,1483p'
```
"},{"role":"user","content":"<stdout>
  1463	
  1464		pendingPredictions.push({ id, timer });
  1465	
  1466		if (pendingPredictions.length > 2) {
  1467			const oldest = pendingPredictions.shift();
  1468			if (oldest) {
  1469				cancelledPredictionIds.add(oldest.id);
  1470				clearTimeout(oldest.timer);
  1471			}
  1472		}
  1473	}
  1474	
  1475	async function autoShowNextAction(): Promise<void> {
  1476		if (suppressAutoPreview) { return; }
  1477		const editor = vscode.window.activeTextEditor;
  1478		if (!editor) { return; }
  1479		try {
  1480			currentAbortController?.abort();
  1481			const controller = new AbortController();
  1482			currentAbortController = controller;
  1483			const requestId = ++latestRequestId;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1583,1603p'
```
"},{"role":"user","content":"<stdout>
  1583			'',
  1584			'=== RESPONSE FORMAT ===',
  1585			'Your response must contain exactly ONE bash code block with one command or two commands connected with &&.',
  1586			'',
  1587			'<format_example>',
  1588			'```bash',
  1589			'your_command_here',
  1590			'```',
  1591			'</format_example>',
  1592			'',
  1593			'Failure to follow these rules will cause your response to be rejected.',
  1594			'',
  1595			'=== EDIT COMMAND FORMAT (IMPORTANT) ===',
  1596			'When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,',
  1597			'and you MUST NOT use substitution commands like \"Ns/old/new/g\".',
  1598			'',
  1599			'Assume all line numbers are 1-based and paths are absolute.',
  1600			'Allowed edit encodings (choose exactly one per response):',
  1601			'',
  1602			'1) Replace a contiguous block of lines:',
  1603			\"   sed -i 'START,ENDc\\\\\",
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1604,1624p'
```","parsedAction":{"kind":"setSelections","selections":[{"start":[1613,2],"end":[1613,2]}]},"avgLogprob":-0.0008589046539086775},"outcome":"accepted","outcomeTimestamp":1765441434749,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765441435302,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n extension-output-pdoom-org.crowd-code-#1-crowd-code
```
"},{"role":"user","content":"<stdout>
     1	9:23:30 AM [info] Activating crowd-code
     2	9:23:30 AM [info] Recording started
     3	9:23:30 AM [info] Initializing git provider using file system watchers...
     4	9:23:30 AM [info] Git repository found
     5	9:23:30 AM [info] Git provider initialized successfully
     6	9:23:30 AM [info] Initial git state: [object Object]
     7	
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1,11p'
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '776,796p'
```
"},{"role":"user","content":"<stdout>
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   790				this.perFileViewport.set(filePath, vp);
   791				shouldEmit = true;
   792			} else {
   793				if (targetLine < vp.start || targetLine > vp.end) {
   794					vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   795					this.perFileViewport.set(filePath, vp);
   796					shouldEmit = true;
</stdout>"},{"role":"assistant","content":"```bash
squeue
```
"},{"role":"user","content":"<stdout>
  JOBID       USER PARTITION NODES  CPUS ST         SUBMIT_TIME          START_TIME     TIME TIME_LIMIT NODELIST(REASON)
  36260   xiao.liu interacti     1   128  R 2025-12-11T07:00:12 2025-12-11T07:00:12  2:23:32   23:59:00 hai006
  36257   xiao.liu interacti     1   128  R 2025-12-11T06:50:04 2025-12-11T06:50:04  2:33:40   23:59:00 hai005
  36249 franz.sram interacti     1    20  R 2025-12-10T16:52:17 2025-12-10T16:52:17 16:31:27 1-00:00:00 hai004
  36256 nishant.ku  standard     3   624  R 2025-12-11T06:46:30 2025-12-11T06:46:38  2:37:06 1-00:00:00 hai[001-003]
  36157   xiao.liu  standard     1   128  R 2025-12-10T09:29:57 2025-12-10T15:15:29 18:08:15   23:59:00 hai007
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1463,1483p'
```
"},{"role":"user","content":"<stdout>
  1463	
  1464		pendingPredictions.push({ id, timer });
  1465	
  1466		if (pendingPredictions.length > 2) {
  1467			const oldest = pendingPredictions.shift();
  1468			if (oldest) {
  1469				cancelledPredictionIds.add(oldest.id);
  1470				clearTimeout(oldest.timer);
  1471			}
  1472		}
  1473	}
  1474	
  1475	async function autoShowNextAction(): Promise<void> {
  1476		if (suppressAutoPreview) { return; }
  1477		const editor = vscode.window.activeTextEditor;
  1478		if (!editor) { return; }
  1479		try {
  1480			currentAbortController?.abort();
  1481			const controller = new AbortController();
  1482			currentAbortController = controller;
  1483			const requestId = ++latestRequestId;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1583,1603p'
```
"},{"role":"user","content":"<stdout>
  1583			'',
  1584			'=== RESPONSE FORMAT ===',
  1585			'Your response must contain exactly ONE bash code block with one command or two commands connected with &&.',
  1586			'',
  1587			'<format_example>',
  1588			'```bash',
  1589			'your_command_here',
  1590			'```',
  1591			'</format_example>',
  1592			'',
  1593			'Failure to follow these rules will cause your response to be rejected.',
  1594			'',
  1595			'=== EDIT COMMAND FORMAT (IMPORTANT) ===',
  1596			'When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,',
  1597			'and you MUST NOT use substitution commands like \"Ns/old/new/g\".',
  1598			'',
  1599			'Assume all line numbers are 1-based and paths are absolute.',
  1600			'Allowed edit encodings (choose exactly one per response):',
  1601			'',
  1602			'1) Replace a contiguous block of lines:',
  1603			\"   sed -i 'START,ENDc\\\\\",
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1666,1686p'
```
"},{"role":"user","content":"<stdout>
  1666			port: cfg.port,
  1667			path: cfg.basePath,
  1668			method: 'POST',
  1669			headers
  1670		};
  1671		if (signal) {
  1672			options.signal = signal;
  1673		}
  1674	
  1675		const json = await new Promise<any>((resolve, reject) => {
  1676			const req = http.request(options, (res: http.IncomingMessage) => {
  1677				let data = '';
  1678				res.on('data', (chunk: Buffer) => { data += chunk.toString(); });
  1679				res.on('end', () => {
  1680					try {
  1681						resolve(JSON.parse(data));
  1682					} catch (err) {
  1683						reject(new Error(`Failed to parse response: ${err instanceof Error ? err.message : String(err)}`));
  1684					}
  1685				});
  1686			});
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1726,1746p'
```","parsedAction":{"kind":"setSelections","selections":[{"start":[1735,33],"end":[1735,33]}]},"avgLogprob":-0.0385381041285254},"outcome":"accepted","outcomeTimestamp":1765441436314,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765441436873,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n extension-output-pdoom-org.crowd-code-#1-crowd-code
```
"},{"role":"user","content":"<stdout>
     1	9:23:30 AM [info] Activating crowd-code
     2	9:23:30 AM [info] Recording started
     3	9:23:30 AM [info] Initializing git provider using file system watchers...
     4	9:23:30 AM [info] Git repository found
     5	9:23:30 AM [info] Git provider initialized successfully
     6	9:23:30 AM [info] Initial git state: [object Object]
     7	
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1,11p'
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '776,796p'
```
"},{"role":"user","content":"<stdout>
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   790				this.perFileViewport.set(filePath, vp);
   791				shouldEmit = true;
   792			} else {
   793				if (targetLine < vp.start || targetLine > vp.end) {
   794					vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   795					this.perFileViewport.set(filePath, vp);
   796					shouldEmit = true;
</stdout>"},{"role":"assistant","content":"```bash
squeue
```
"},{"role":"user","content":"<stdout>
  JOBID       USER PARTITION NODES  CPUS ST         SUBMIT_TIME          START_TIME     TIME TIME_LIMIT NODELIST(REASON)
  36260   xiao.liu interacti     1   128  R 2025-12-11T07:00:12 2025-12-11T07:00:12  2:23:32   23:59:00 hai006
  36257   xiao.liu interacti     1   128  R 2025-12-11T06:50:04 2025-12-11T06:50:04  2:33:40   23:59:00 hai005
  36249 franz.sram interacti     1    20  R 2025-12-10T16:52:17 2025-12-10T16:52:17 16:31:27 1-00:00:00 hai004
  36256 nishant.ku  standard     3   624  R 2025-12-11T06:46:30 2025-12-11T06:46:38  2:37:06 1-00:00:00 hai[001-003]
  36157   xiao.liu  standard     1   128  R 2025-12-10T09:29:57 2025-12-10T15:15:29 18:08:15   23:59:00 hai007
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1463,1483p'
```
"},{"role":"user","content":"<stdout>
  1463	
  1464		pendingPredictions.push({ id, timer });
  1465	
  1466		if (pendingPredictions.length > 2) {
  1467			const oldest = pendingPredictions.shift();
  1468			if (oldest) {
  1469				cancelledPredictionIds.add(oldest.id);
  1470				clearTimeout(oldest.timer);
  1471			}
  1472		}
  1473	}
  1474	
  1475	async function autoShowNextAction(): Promise<void> {
  1476		if (suppressAutoPreview) { return; }
  1477		const editor = vscode.window.activeTextEditor;
  1478		if (!editor) { return; }
  1479		try {
  1480			currentAbortController?.abort();
  1481			const controller = new AbortController();
  1482			currentAbortController = controller;
  1483			const requestId = ++latestRequestId;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1583,1603p'
```
"},{"role":"user","content":"<stdout>
  1583			'',
  1584			'=== RESPONSE FORMAT ===',
  1585			'Your response must contain exactly ONE bash code block with one command or two commands connected with &&.',
  1586			'',
  1587			'<format_example>',
  1588			'```bash',
  1589			'your_command_here',
  1590			'```',
  1591			'</format_example>',
  1592			'',
  1593			'Failure to follow these rules will cause your response to be rejected.',
  1594			'',
  1595			'=== EDIT COMMAND FORMAT (IMPORTANT) ===',
  1596			'When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,',
  1597			'and you MUST NOT use substitution commands like \"Ns/old/new/g\".',
  1598			'',
  1599			'Assume all line numbers are 1-based and paths are absolute.',
  1600			'Allowed edit encodings (choose exactly one per response):',
  1601			'',
  1602			'1) Replace a contiguous block of lines:',
  1603			\"   sed -i 'START,ENDc\\\\\",
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1666,1686p'
```
"},{"role":"user","content":"<stdout>
  1666			port: cfg.port,
  1667			path: cfg.basePath,
  1668			method: 'POST',
  1669			headers
  1670		};
  1671		if (signal) {
  1672			options.signal = signal;
  1673		}
  1674	
  1675		const json = await new Promise<any>((resolve, reject) => {
  1676			const req = http.request(options, (res: http.IncomingMessage) => {
  1677				let data = '';
  1678				res.on('data', (chunk: Buffer) => { data += chunk.toString(); });
  1679				res.on('end', () => {
  1680					try {
  1681						resolve(JSON.parse(data));
  1682					} catch (err) {
  1683						reject(new Error(`Failed to parse response: ${err instanceof Error ? err.message : String(err)}`));
  1684					}
  1685				});
  1686			});
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1793,1813p'
```
"},{"role":"user","content":"<stdout>
  1793			return undefined;
  1794		}
  1795		const script = sedMatch[1] ?? '';
  1796		const targetFile = sedMatch[2] ?? '';
  1797		const activePath = doc.uri.fsPath;
  1798		if (targetFile !== activePath) {
  1799			return undefined;
  1800		}
  1801	
  1802		// Delete: \"START,ENDd\"
  1803		const deleteMatch = script.match(/^(\\d+),(\\d+)d$/);
  1804		if (deleteMatch) {
  1805			const startLine1 = Number(deleteMatch[1]);
  1806			const endLine1 = Number(deleteMatch[2]);
  1807			if (!Number.isFinite(startLine1) || !Number.isFinite(endLine1)) {
  1808				return undefined;
  1809			}
  1810			const startLine0 = Math.max(0, startLine1 - 1);
  1811			const endLine0 = Math.max(0, endLine1 - 1);
  1812	
  1813			let endPosLine = endLine0 + 1;
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1814,1834p'
```","parsedAction":{"kind":"setSelections","selections":[{"start":[1823,54],"end":[1823,54]}]},"avgLogprob":-0.0002995824052009409},"outcome":"ignored","outcomeTimestamp":1765441443198,"modelName":"qwen/qwen3-8b"}
{"timestamp":1765441443198,"context":[{"role":"system","content":"You are a helpful assistant that interacts with a computer shell to solve programming tasks.
Your goal is to predict the next bash command a developer would most likely execute, given their editing and navigation history.

=== CONVERSATION FORMAT ===
The conversation history alternates between:
- Assistant messages: bash commands in fenced code blocks
- User messages: command output wrapped in <stdout>...</stdout> tags

File contents are displayed with 6-character right-aligned line numbers followed by a tab, e.g.:
     1	first line
     2	second line

File content is typically shown in viewports of ~20 lines around the area of interest.

=== RESPONSE FORMAT ===
Your response must contain exactly ONE bash code block with one command or two commands connected with &&.

<format_example>
```bash
your_command_here
```
</format_example>

Failure to follow these rules will cause your response to be rejected.

=== EDIT COMMAND FORMAT (IMPORTANT) ===
When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,
and you MUST NOT use substitution commands like \"Ns/old/new/g\".

Assume all line numbers are 1-based and paths are absolute.
Allowed edit encodings (choose exactly one per response):

1) Replace a contiguous block of lines:
   sed -i 'START,ENDc\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

2) Delete a contiguous block of lines:
   sed -i 'START,ENDd' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

3) Insert new lines BEFORE a given line:
   sed -i 'STARTi\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

4) Append new lines at the END of the file:
   sed -i '$a\\
NEW_LINE_1
NEW_LINE_2
...
' /abs/path/to/file && cat -n /abs/path/to/file | sed -n 'VSTART,VENDp'

Where VSTART and VEND specify a small viewport around the edited region.

Do NOT emit commands like \"3s/print/print()/g\" or any other \"s/old/new/\" style sed substitution; instead,
always rewrite the affected lines using one of the line-based forms above.

When you are NOT editing files (e.g., running tests, git commands, tools, etc.), you may emit arbitrary bash commands."},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
    12		completion: {
    13			rawModelOutput: string;
    14			parsedAction: Action | null;
    15			avgLogprob: number;
    16		};
    17		outcome: 'accepted' | 'rejected' | 'ignored' | null;
    18		outcomeTimestamp: number | null;
    19		modelName: string;
    20	}
    21	
    22	interface PendingPreferenceSample {
    23		sample: PreferenceSample;
    24		shownAt: number;
    25	}
    26	
    27	let pendingPreferenceSample: PendingPreferenceSample | null = null;
    28	
    29	function getPreferenceLogPath(): string {
    30		const cfg = getConfig();
    31		if (cfg.preferenceLogPath) {
    32			return cfg.preferenceLogPath;
    33		}
    34		const workspaceFolders = vscode.workspace.workspaceFolders;
    35		if (workspaceFolders) {
    36			return path.join(workspaceFolders[0].uri.fsPath, '.crowd-pilot-preferences.jsonl');
    37		}
    38		throw new Error(\"No preference log path found.\")
    39	}
    40	
    41	/**
    42	 * Log a preference sample to the JSONL file.
    43	 * Each line is a complete JSON object for easy streaming/parsing.
    44	 */
    45	function logPreferenceSample(sample: PreferenceSample): void {
    46		const cfg = getConfig();
    47		if (!cfg.enablePreferenceLogging) {
    48			console.log(`[crowd-pilot] Preference logging disabled, skipping sample`);
    49			return;
    50		}
    51	
    52		const logPath = getPreferenceLogPath();
    53		const line = JSON.stringify(sample) + '
    54	';
    55		
    56		fs.appendFile(logPath, line, (err) => {
    57			if (err) {
    58				console.error('[crowd-pilot] Failed to log preference sample:', err);
    59			} else {
    60				console.log(`[crowd-pilot] Logged preference sample, outcome: (${sample.outcome})`);
    61			}
    62		});
    63	}
    64	
    65	/**
    66	 * Create a new pending preference sample when showing a preview.
    67	 * This captures all context needed for reward model training.
    68	 */
    69	function createPendingPreferenceSample(
    70		conversationMessages: Array<{ role: string; content: string }>,
    71		rawModelOutput: string,
    72		parsedAction: Action | null,
    73		avgLogprob: number,
    74		modelName: string
    75	): void {
    76		const sample: PreferenceSample = {
    77			timestamp: Date.now(),
    78			context: conversationMessages,
    79			completion: {
    80				rawModelOutput,
    81				parsedAction,
    82				avgLogprob,
    83			},
    84			outcome: null,
    85			outcomeTimestamp: null,
    86			modelName,
    87		};
    88	
    89		pendingPreferenceSample = {
    90			sample,
    91			shownAt: Date.now(),
    92		};
    93	}
    94	
    95	/**
    96	 * Record the outcome of the current pending sample and log it.
    97	 */
    98	function recordPreferenceOutcome(outcome: 'accepted' | 'rejected' | 'ignored'): void {
    99		if (!pendingPreferenceSample) {
   100			return;
   101		}
   102	
   103		const sample = pendingPreferenceSample.sample;
   104		sample.outcome = outcome;
   105		sample.outcomeTimestamp = Date.now();
   106	
   107		logPreferenceSample(sample);
   108	
   109		pendingPreferenceSample = null;
   110	}
   111	
   112	/**
   113	 * Mark any pending sample as ignored (user moved on without explicit accept/reject).
   114	 */
   115	function markPendingAsIgnored(): void {
   116		if (pendingPreferenceSample) {
   117			recordPreferenceOutcome('ignored');
   118		}
   119	}
   120	
   121	type Action =
   122	| { kind: 'showTextDocument' }
   123	| { kind: 'setSelections', selections: Array<{ start: [number, number], end: [number, number] }> }
   124	| { kind: 'editInsert', position: [number, number], text: string }
   125	| { kind: 'editDelete', range: { start: [number, number], end: [number, number] } }
   126	| { kind: 'editReplace', range: { start: [number, number], end: [number, number] }, text: string }
   127	| { kind: 'terminalShow' }
   128	| { kind: 'terminalSendText', text: string };
   129	
   130	// Configuration helper
   131	function getConfig() {
   132		const config = vscode.workspace.getConfiguration('crowd-pilot');
   133		return {
   134			hostname: config.get<string>('hostname', 'hai001'),
   135			port: config.get<number>('port', 30000),
   136			basePath: config.get<string>('basePath', '/v1/chat/completions'),
   137			modelName: config.get<string>('modelName', 'qwen/qwen3-8b'),
   138			minAvgLogprob: config.get<number>('minAvgLogprob', -1.0),
   139			maxContextTokens: config.get<number>('maxContextTokens', 120000),
   140			preferenceLogPath: config.get<string>('preferenceLogPath', ''),
   141			enablePreferenceLogging: config.get<boolean>('enablePreferenceLogging', true),
   142		};
   143	}
   144	
   145	// -------------------- Context Window Management --------------------
   146	
   147	/**
   148	 * Estimate token count from text using character-based heuristic.
   149	 * Uses ~4 characters per token as a rough approximation for most LLMs.
   150	 */
   151	function estimateTokens(text: string): number {
   152		return Math.ceil(text.length / 4);
   153	}
   154	
   155	/**
   156	 * Truncate conversation messages to fit within the context window.
   157	 * Assumes system prompt is the first message. Drops oldest conversation messages first.
   158	 */
   159	function truncateToContextLimit(
   160		messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
   161		maxTokens: number
   162	): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
   163		if (messages.length === 0) { return messages; }
   164	
   165		const systemTokens = estimateTokens(messages[0].content);
   166		const availableTokens = maxTokens - systemTokens;
   167	
   168		const tokenCounts = messages.slice(1).map(m => estimateTokens(m.content));
   169		const totalConversationTokens = tokenCounts.reduce((a, b) => a + b, 0);
   170	
   171		if (totalConversationTokens <= availableTokens) {
   172			return messages;
   173		}
   174	
   175		let keptTokens = 0;
   176		let cutoffIndex = tokenCounts.length;
   177		for (let i = tokenCounts.length - 1; i >= 0; i--) {
   178			if (keptTokens + tokenCounts[i] <= availableTokens) {
   179				keptTokens += tokenCounts[i];
   180				cutoffIndex = i;
   181			} else {
   182				break;
   183			}
   184		}
   185	
   186		console.log(`[crowd-pilot] Truncated ${cutoffIndex} oldest messages (${systemTokens + totalConversationTokens} -> ${systemTokens + keptTokens} tokens)`);
   187		return [messages[0], ...messages.slice(cutoffIndex + 1)];
   188	}
   189	
   190	// -------------------- Serialization Helpers (mirrors serialization_utils.py) --------------------
   191	const ANSI_CSI_RE = /\\x1b\\[[0-9;?]*[ -/]*[@-~]/g;
   192	const ANSI_OSC_TERMINATED_RE = /\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)/g;
   193	const ANSI_OSC_LINE_FALLBACK_RE = /\\x1b\\][^
   194	]*$/g;
   195	
   196	// NOTE (f.srambical): Make sure that these are the parameters that were used during serialization
   197	const VIEWPORT_RADIUS = 10;
   198	const COALESCE_RADIUS = 5;
   199	// FIXME (f.srambical): this slightly differs from the logic in @file:crowd-pilot/crowd_pilot/serialization_utils.py
   200	// as we only approximate the token count
   201	const MAX_TOKENS_PER_MESSAGE = 2048;
   202	
   203	function cleanText(text: string): string {
   204		return text.replace(/
   205	/g, '
   206	').replace(/
/g, '
   207	').trimEnd();
   208	}
   209	
   210	function fencedBlock(language: string | null, content: string): string {
   211		const lang = (language || '').toLowerCase();
   212		return `\\`\\`\\`${lang}
   213	${content}
   214	\\`\\`\\`
   215	`;
   216	}
   217	
   218	function applyChange(content: string, offset: number, length: number, newText: string): string {
   219		let base = String(content);
   220		const text = (newText ?? '').replace(/\\
   "},{"role":"assistant","content":"```bash
cat -n extension-output-pdoom-org.crowd-code-#1-crowd-code
```
"},{"role":"user","content":"<stdout>
     1	9:23:30 AM [info] Activating crowd-code
     2	9:23:30 AM [info] Recording started
     3	9:23:30 AM [info] Initializing git provider using file system watchers...
     4	9:23:30 AM [info] Git repository found
     5	9:23:30 AM [info] Git provider initialized successfully
     6	9:23:30 AM [info] Initial git state: [object Object]
     7	
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1,11p'
```
"},{"role":"user","content":"<stdout>
     1	import * as vscode from 'vscode';
     2	import * as http from 'http';
     3	import * as fs from 'fs';
     4	import * as path from 'path';
     5	import { Buffer } from 'buffer';
     6	
     7	// -------------------- Preference Data Collection --------------------
     8	
     9	interface PreferenceSample {
    10		timestamp: number;
    11		context: Array<{ role: string; content: string }>;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '776,796p'
```
"},{"role":"user","content":"<stdout>
   776	
   777			this.flushTerminalOutputBuffer();
   778	
   779			const content = this.fileStates.get(filePath) ?? '';
   780			const totalLines = content.split(/
?
   781	/).length;
   782			const targetLine = content.slice(0, offset).split('
   783	').length;
   784	
   785			let vp = this.perFileViewport.get(filePath);
   786			let shouldEmit = false;
   787	
   788			if (!vp || vp.end === 0) {
   789				vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   790				this.perFileViewport.set(filePath, vp);
   791				shouldEmit = true;
   792			} else {
   793				if (targetLine < vp.start || targetLine > vp.end) {
   794					vp = serializeComputeViewport(totalLines, targetLine, VIEWPORT_RADIUS);
   795					this.perFileViewport.set(filePath, vp);
   796					shouldEmit = true;
</stdout>"},{"role":"assistant","content":"```bash
squeue
```
"},{"role":"user","content":"<stdout>
  JOBID       USER PARTITION NODES  CPUS ST         SUBMIT_TIME          START_TIME     TIME TIME_LIMIT NODELIST(REASON)
  36260   xiao.liu interacti     1   128  R 2025-12-11T07:00:12 2025-12-11T07:00:12  2:23:32   23:59:00 hai006
  36257   xiao.liu interacti     1   128  R 2025-12-11T06:50:04 2025-12-11T06:50:04  2:33:40   23:59:00 hai005
  36249 franz.sram interacti     1    20  R 2025-12-10T16:52:17 2025-12-10T16:52:17 16:31:27 1-00:00:00 hai004
  36256 nishant.ku  standard     3   624  R 2025-12-11T06:46:30 2025-12-11T06:46:38  2:37:06 1-00:00:00 hai[001-003]
  36157   xiao.liu  standard     1   128  R 2025-12-10T09:29:57 2025-12-10T15:15:29 18:08:15   23:59:00 hai007
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1463,1483p'
```
"},{"role":"user","content":"<stdout>
  1463	
  1464		pendingPredictions.push({ id, timer });
  1465	
  1466		if (pendingPredictions.length > 2) {
  1467			const oldest = pendingPredictions.shift();
  1468			if (oldest) {
  1469				cancelledPredictionIds.add(oldest.id);
  1470				clearTimeout(oldest.timer);
  1471			}
  1472		}
  1473	}
  1474	
  1475	async function autoShowNextAction(): Promise<void> {
  1476		if (suppressAutoPreview) { return; }
  1477		const editor = vscode.window.activeTextEditor;
  1478		if (!editor) { return; }
  1479		try {
  1480			currentAbortController?.abort();
  1481			const controller = new AbortController();
  1482			currentAbortController = controller;
  1483			const requestId = ++latestRequestId;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1583,1603p'
```
"},{"role":"user","content":"<stdout>
  1583			'',
  1584			'=== RESPONSE FORMAT ===',
  1585			'Your response must contain exactly ONE bash code block with one command or two commands connected with &&.',
  1586			'',
  1587			'<format_example>',
  1588			'```bash',
  1589			'your_command_here',
  1590			'```',
  1591			'</format_example>',
  1592			'',
  1593			'Failure to follow these rules will cause your response to be rejected.',
  1594			'',
  1595			'=== EDIT COMMAND FORMAT (IMPORTANT) ===',
  1596			'When you want to EDIT a file, you MUST encode the edit using line-based sed commands in ONE of the following forms,',
  1597			'and you MUST NOT use substitution commands like \"Ns/old/new/g\".',
  1598			'',
  1599			'Assume all line numbers are 1-based and paths are absolute.',
  1600			'Allowed edit encodings (choose exactly one per response):',
  1601			'',
  1602			'1) Replace a contiguous block of lines:',
  1603			\"   sed -i 'START,ENDc\\\\\",
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1666,1686p'
```
"},{"role":"user","content":"<stdout>
  1666			port: cfg.port,
  1667			path: cfg.basePath,
  1668			method: 'POST',
  1669			headers
  1670		};
  1671		if (signal) {
  1672			options.signal = signal;
  1673		}
  1674	
  1675		const json = await new Promise<any>((resolve, reject) => {
  1676			const req = http.request(options, (res: http.IncomingMessage) => {
  1677				let data = '';
  1678				res.on('data', (chunk: Buffer) => { data += chunk.toString(); });
  1679				res.on('end', () => {
  1680					try {
  1681						resolve(JSON.parse(data));
  1682					} catch (err) {
  1683						reject(new Error(`Failed to parse response: ${err instanceof Error ? err.message : String(err)}`));
  1684					}
  1685				});
  1686			});
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1793,1813p'
```
"},{"role":"user","content":"<stdout>
  1793			return undefined;
  1794		}
  1795		const script = sedMatch[1] ?? '';
  1796		const targetFile = sedMatch[2] ?? '';
  1797		const activePath = doc.uri.fsPath;
  1798		if (targetFile !== activePath) {
  1799			return undefined;
  1800		}
  1801	
  1802		// Delete: \"START,ENDd\"
  1803		const deleteMatch = script.match(/^(\\d+),(\\d+)d$/);
  1804		if (deleteMatch) {
  1805			const startLine1 = Number(deleteMatch[1]);
  1806			const endLine1 = Number(deleteMatch[2]);
  1807			if (!Number.isFinite(startLine1) || !Number.isFinite(endLine1)) {
  1808				return undefined;
  1809			}
  1810			const startLine0 = Math.max(0, startLine1 - 1);
  1811			const endLine0 = Math.max(0, endLine1 - 1);
  1812	
  1813			let endPosLine = endLine0 + 1;
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/package.json
```
"},{"role":"user","content":"<stdout>
     1	{
     2	  \"name\": \"crowd-pilot\",
     3	  \"displayName\": \"crowd-pilot-extension\",
     4	  \"description\": \"Teaching language models to code like humans.\",
     5	  \"publisher\": \"p-doom\",
     6	  \"version\": \"0.0.1\",
     7	  \"repository\": {
     8	    \"type\": \"git\",
     9	    \"url\": \"https://github.com/p-doom/crowd-pilot-extension\"
    10	  },
    11	  \"engines\": {
    12	    \"vscode\": \"^1.99.3\"
    13	  },
    14	  \"categories\": [
    15	    \"Other\"
    16	  ],
    17	  \"activationEvents\": [
    18	    \"onStartupFinished\"
    19	  ],
    20	  \"main\": \"./out/extension.js\",
    21	  \"contributes\": {
    22	    \"commands\": [
    23	      {
    24	        \"command\": \"crowd-pilot.toggleSuggestions\",
    25	        \"title\": \"crowd-pilot: Toggle Tab Suggestions\"
    26	      },
    27	      {
    28	        \"command\": \"crowd-pilot.hideUi\",
    29	        \"title\": \"crowd-pilot: Hide Preview\"
    30	      },
    31	      {
    32	        \"command\": \"crowd-pilot.sglangTest\",
    33	        \"title\": \"crowd-pilot: Test SGLang\"
    34	      },
    35	      {
    36	        \"command\": \"crowd-pilot.modelRun\",
    37	        \"title\": \"crowd-pilot: Model Plan & Run\"
    38	      },
    39	      {
    40	        \"command\": \"crowd-pilot.clearContext\",
    41	        \"title\": \"crowd-pilot: Clear Context\"
    42	      },
    43	      {
    44	        \"command\": \"crowd-pilot.openPreferenceLog\",
    45	        \"title\": \"crowd-pilot: Open Preference Log\"
    46	      }
    47	    ],
    48	    \"configuration\": {
    49	      \"title\": \"crowd-pilot\",
    50	      \"properties\": {
    51	        \"crowd-pilot.hostname\": {
    52	          \"type\": \"string\",
    53	          \"default\": \"hai002\",
    54	          \"description\": \"Hostname of the SGLang server\"
    55	        },
    56	        \"crowd-pilot.port\": {
    57	          \"type\": \"number\",
    58	          \"default\": 30000,
    59	          \"description\": \"Port of the SGLang server\"
    60	        },
    61	        \"crowd-pilot.basePath\": {
    62	          \"type\": \"string\",
    63	          \"default\": \"/v1/chat/completions\",
    64	          \"description\": \"Base path for the SGLang API endpoint\"
    65	        },
    66	        \"crowd-pilot.modelName\": {
    67	          \"type\": \"string\",
    68	          \"default\": \"qwen/qwen3-8b\",
    69	          \"description\": \"Model name to use for completions\"
    70	        },
    71	        \"crowd-pilot.minAvgLogprob\": {
    72	          \"type\": \"number\",
    73	          \"default\": -1.0,
    74	          \"description\": \"Minimum average log-probability per token for displaying suggestions. Higher values (closer to 0) require more confidence. -1.0 ≈ perplexity 2.7\"
    75	        },
    76	        \"crowd-pilot.maxContextTokens\": {
    77	          \"type\": \"number\",
    78	          \"default\": 120000,
    79	          \"description\": \"Context length (in tokens). Older messages are truncated to fit. Set below your model's limit to leave room for the response.\"
    80	        },
    81	        \"crowd-pilot.enablePreferenceLogging\": {
    82	          \"type\": \"boolean\",
    83	          \"default\": true,
    84	          \"description\": \"Enable logging of accept/reject data for reward model training and RLHF/DPO\"
    85	        },
    86	        \"crowd-pilot.preferenceLogPath\": {
    87	          \"type\": \"string\",
    88	          \"default\": \"\",
    89	          \"description\": \"Custom path for the preference log file (JSONL format). If empty, uses workspace/.crowd-pilot-preferences.jsonl\"
    90	        }
    91	      }
    92	    },
    93	    \"keybindings\": [
    94	      {
    95	        \"command\": \"crowd-pilot.modelRun\",
    96	        \"key\": \"tab\",
    97	        \"mac\": \"tab\",
    98	        \"when\": \"editorTextFocus && crowdPilot.uiVisible\"
    99	      },
   100	      {
   101	        \"command\": \"crowd-pilot.modelRun\",
   102	        \"key\": \"tab\",
   103	        \"mac\": \"tab\",
   104	        \"when\": \"inQuickOpen && crowdPilot.uiVisible\"
   105	      },
   106	      {
   107	        \"command\": \"crowd-pilot.hideUi\",
   108	        \"key\": \"escape\",
   109	        \"mac\": \"escape\",
   110	        \"when\": \"crowdPilot.uiVisible\"
   111	      }
   112	    ]
   113	  },
   114	  \"scripts\": {
   115	    \"vscode:prepublish\": \"npm run compile\",
   116	    \"compile\": \"tsc -p ./\",
   117	    \"watch\": \"tsc -watch -p ./\",
   118	    \"pretest\": \"npm run compile && npm run lint\",
   119	    \"lint\": \"eslint src\",
   120	    \"test\": \"vscode-test\"
   121	  },
   122	  \"devDependencies\": {
   123	    \"@types/vscode\": \"^1.99.3\",
   124	    \"@types/mocha\": \"^10.0.10\",
   125	    \"@types/node\": \"22.x\",
   126	    \"@typescript-eslint/eslint-plugin\": \"^8.45.0\",
   127	    \"@typescript-eslint/parser\": \"^8.45.0\",
   128	    \"eslint\": \"^9.36.0\",
   129	    \"typescript\": \"^5.9.3\",
   130	    \"@vscode/test-cli\": \"^0.0.11\",
   131	    \"@vscode/test-electron\": \"^2.5.2\"
   132	  }
   133	}
   134	
</stdout>"},{"role":"assistant","content":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/package.json | sed -n '70,90p'
```
"},{"role":"user","content":"<stdout>
    70	        },
    71	        \"crowd-pilot.minAvgLogprob\": {
    72	          \"type\": \"number\",
    73	          \"default\": -1.0,
    74	          \"description\": \"Minimum average log-probability per token for displaying suggestions. Higher values (closer to 0) require more confidence. -1.0 ≈ perplexity 2.7\"
    75	        },
    76	        \"crowd-pilot.maxContextTokens\": {
    77	          \"type\": \"number\",
    78	          \"default\": 120000,
    79	          \"description\": \"Context length (in tokens). Older messages are truncated to fit. Set below your model's limit to leave room for the response.\"
    80	        },
    81	        \"crowd-pilot.enablePreferenceLogging\": {
    82	          \"type\": \"boolean\",
    83	          \"default\": true,
    84	          \"description\": \"Enable logging of accept/reject data for reward model training and RLHF/DPO\"
    85	        },
    86	        \"crowd-pilot.preferenceLogPath\": {
    87	          \"type\": \"string\",
    88	          \"default\": \"\",
    89	          \"description\": \"Custom path for the preference log file (JSONL format). If empty, uses workspace/.crowd-pilot-preferences.jsonl\"
    90	        }
</stdout>"}],"completion":{"rawModelOutput":"```bash
cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1814,1834p'
```
```bash
```","parsedAction":{"kind":"terminalSendText","text":"cat -n /home/franz.srambical/crowd-pilot-extension/src/extension.ts | sed -n '1814,1834p'"},"avgLogprob":-0.07702164511585805},"outcome":"rejected","outcomeTimestamp":1765441466554,"modelName":"qwen/qwen3-8b"}