@volter/supercode-teams 0.3.106 → 0.3.107

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/codex-startup.mjs CHANGED
@@ -131,3 +131,30 @@ export async function settleCodexHookReview(host, sessionId, { signal, deadlineM
131
131
  }
132
132
  return signal.aborted ? 'aborted' : 'deadline';
133
133
  }
134
+
135
+ /** The most one bracketed paste carries into a Windows Codex: ConPTY and Codex drop a whole paste past about 8190
136
+ * characters (measured 2026-10-06, codex-cli 0.160.1: 8180 arrived, 8200 did not), while consecutive pastes join
137
+ * into one composer entry. */
138
+ export const CODEX_WINDOWS_PASTE_CHARS = 8000;
139
+
140
+ /** One character as a Windows Codex reads it from its console. Codex drops some BMP characters that arrive as plain
141
+ * text through ConPTY (measured: `—`, `…`, `✓` vanish while `é`, `日`, and astral `🙂` arrive), so every BMP
142
+ * character past ASCII goes as a win32-input-mode key event carrying its UTF-16 unit (VK_PACKET, 231), which ConPTY
143
+ * hands Codex as exactly that character. ASCII and astral characters go as they are. */
144
+ function codexWindowsCharacter(character) {
145
+ const unit = character.codePointAt(0);
146
+ if (unit < 0x80 || unit > 0xffff) return character;
147
+ return `\u001b[231;0;${unit};1;0;1_\u001b[231;0;${unit};0;0;1_`;
148
+ }
149
+
150
+ /** Text as the raw writes that paste it into a Windows Codex composer: bracketed pastes of at most
151
+ * CODEX_WINDOWS_PASTE_CHARS characters each, non-ASCII BMP characters as key events. Written as they are (a terminal
152
+ * host turns `\n` into `\r`, which Codex keeps as a line break inside a paste); never submitted here. */
153
+ export function codexWindowsPastes(text) {
154
+ const characters = Array.from(String(text));
155
+ const pastes = [];
156
+ for (let at = 0; at < characters.length; at += CODEX_WINDOWS_PASTE_CHARS) {
157
+ pastes.push(`\u001b[200~${characters.slice(at, at + CODEX_WINDOWS_PASTE_CHARS).map(codexWindowsCharacter).join('')}\u001b[201~`);
158
+ }
159
+ return pastes;
160
+ }
package/machine.mjs CHANGED
@@ -93,7 +93,7 @@ import { JobOwner, executionCapabilities, streamProgram, serveWorkspace } from '
93
93
  import { TerminalMailbox, retainedTerminalMail } from './terminal-mail.mjs';
94
94
  import { confirmNativeInitialInput } from './native-initial-input.mjs';
95
95
  import { sessionPeople } from './session-people.mjs';
96
- import { canonicalCodexFolderTrust, codexProgram, codexStartupArguments, codexUpdatePrompt, codexVersion, settleCodexHookReview } from './codex-startup.mjs';
96
+ import { canonicalCodexFolderTrust, codexProgram, codexStartupArguments, codexUpdatePrompt, codexVersion, codexWindowsPastes, settleCodexHookReview } from './codex-startup.mjs';
97
97
  import { sessionNoticeTarget } from './session-notice-target.mjs';
98
98
  import { serveAppDoor } from './app-door.mjs';
99
99
 
@@ -203,6 +203,26 @@ const TURN_CONFIRM_MS = 60_000;
203
203
  * as the program (`open --new "npm run build"`), that line through this machine's own shell, so a caller on
204
204
  * another machine need not know which shell or platform this one has.
205
205
  */
206
+ /** How long the Windows pane host's command line for `program args` is, quoted as it quotes a batch program's
207
+ * arguments (sdk/terminal `batchArgument`: `"` doubled, a `\` run before a `"` doubled, `%` as `%%cd:~,%`), plus
208
+ * the `cmd /e:ON /v:OFF /d /s /c "…"` around it. An upper bound for a program cmd does not run. */
209
+ function windowsCommandLineLength(program, args) {
210
+ const quoted = (value) => {
211
+ let length = 2;
212
+ let backslashes = 0;
213
+ for (const character of String(value)) {
214
+ if (character === '\\') backslashes += 1;
215
+ else {
216
+ if (character === '"') length += backslashes + 1;
217
+ backslashes = 0;
218
+ }
219
+ length += character === '%' ? 8 : 1;
220
+ }
221
+ return length + backslashes;
222
+ };
223
+ return [program, ...args].reduce((total, value) => total + quoted(value) + 1, 0) + 32;
224
+ }
225
+
206
226
  function launchCommand(program, args) {
207
227
  const line = typeof program === 'string' ? program.trim() : '';
208
228
  if (!args.length && /\s/.test(line) && !existsSync(line)) {
@@ -1782,20 +1802,24 @@ export class TeamsMachine extends MachineCore {
1782
1802
  const name = String(params.program ?? '').split(/[\\/]/).pop();
1783
1803
  // Windows has no Unix endpoint for a pane's own native server, so a fresh Codex there starts with no session to
1784
1804
  // type into: its rollout appears only with its first turn. Its first input is therefore Codex's own argv prompt,
1785
- // as it is on the other platforms, and the receipt is the rollout that records it under CODEX_HOME. A batch
1786
- // command line cannot carry a line break, so multi-line input keeps the pane and is typed as before.
1805
+ // as it is on the other platforms, and the receipt is the rollout that records it under CODEX_HOME. Input a
1806
+ // command line cannot carry — a line break (a batch line ends there), or past cmd's 8191 characters
1807
+ // (CreateProcess's 32767 for an .exe) — is pasted into the composer once it is up instead
1808
+ // (#typeCodexFirstInput), under the same receipt.
1787
1809
  if (process.platform === 'win32' && params.kind === 'codex' && /^codex(?:\.cmd|\.exe)?$/i.test(name)) {
1788
- const initialInput = params.initialInput && !/[\r\n\0]/.test(params.initialInput) && !args.includes('--') &&
1810
+ const initialInput = params.initialInput && !args.includes('--') &&
1789
1811
  !args.some((arg) => arg === '--remote' || arg.startsWith('--remote=')) ? params.initialInput : null;
1790
1812
  if (!initialInput) return launchCommand(params.program, args);
1813
+ const typed = /[\r\n\0]/.test(initialInput) || windowsCommandLineLength(params.program, [...args, '--', initialInput]) >
1814
+ (/\.(cmd|bat)$/i.test(params.program) ? 8000 : 32000);
1791
1815
  const codexHome = params.env?.CODEX_HOME || this.env.CODEX_HOME || join(homedir(), '.codex');
1792
1816
  // A resume appends to the rollout it names (`resume <id>`; `--last` names none). A fork writes a new
1793
1817
  // rollout, so its receipt is found as a fresh session's is.
1794
1818
  const verb = args.indexOf('resume');
1795
1819
  const named = verb >= 0 ? args.slice(verb + 1).find((arg) => /^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(arg)) : null;
1796
1820
  nativeInput({ kind: params.kind, text: initialInput, startedAt: Date.now(), codexSessions: join(codexHome, 'sessions'),
1797
- cwd: params.cwd || null, continuing: verb >= 0, resumeId: named ?? null });
1798
- return launchCommand(params.program, [...args, '--', initialInput]);
1821
+ cwd: params.cwd || null, continuing: verb >= 0, resumeId: named ?? null, typed });
1822
+ return launchCommand(params.program, typed ? args : [...args, '--', initialInput]);
1799
1823
  }
1800
1824
  if (process.platform === 'win32' || !((params.kind === 'codex' && name === 'codex') ||
1801
1825
  (params.kind === 'claude-code' && name === 'claude'))) return launchCommand(params.program, args);
@@ -2093,11 +2117,26 @@ export class TeamsMachine extends MachineCore {
2093
2117
  #deliverInitialInput({ key, contextKey, session, kind, by, input, act, nativeInput, executable = {}, resumed = {} }) {
2094
2118
  const abort = new AbortController();
2095
2119
  const task = (async () => {
2120
+ let nativeReadyBy = 'native-prompt';
2121
+ if (nativeInput?.typed) {
2122
+ // A Windows Codex input its command line could not carry: pasted once the composer is up, and confirmed by
2123
+ // the same rollout receipt as an argv prompt. Its pane has no native session to mail yet.
2124
+ nativeReadyBy = await this.#awaitInputReady(session, contextKey, kind, abort.signal);
2125
+ if (nativeReadyBy === 'aborted') return;
2126
+ const typed = nativeReadyBy === 'pane-gone' || nativeReadyBy === 'deadline' ? { delivered: false, reason: nativeReadyBy }
2127
+ : await this.#typeCodexFirstInput(session, input, abort.signal);
2128
+ if (!typed.delivered) {
2129
+ this.launches.record({ key, pane: contextKey, kind, by, ...resumed, ...executable, status: 'ready', inputDelivered: false,
2130
+ inputReadyBy: nativeReadyBy, inputError: typed.reason });
2131
+ act('launch', `pane:${contextKey}`, 'failed', `first input undelivered: ${typed.reason}`);
2132
+ return;
2133
+ }
2134
+ }
2096
2135
  if (nativeInput) {
2097
2136
  const result = await confirmNativeInitialInput(nativeInput, { signal: abort.signal,
2098
2137
  deadlineMs: Date.now() + INITIAL_INPUT_READY_DEADLINE_MS, live: async () => Boolean(await this.#paneByContext(contextKey)) });
2099
2138
  this.launches.record({ key, pane: contextKey, kind, by, ...resumed, ...executable, status: 'ready', inputDelivered: result.delivered,
2100
- inputReadyBy: 'native-prompt', ...(result.delivered ? { inputReceipt: result.receipt } : { inputError: result.reason }),
2139
+ inputReadyBy: nativeReadyBy, ...(result.delivered ? { inputReceipt: result.receipt } : { inputError: result.reason }),
2101
2140
  ...(result.native_session ? { native_session: result.native_session } : {}) });
2102
2141
  act('launch', `pane:${contextKey}`, result.delivered ? 'ok' : result.queued ? 'input-pending' : 'failed',
2103
2142
  result.delivered ? 'first input confirmed by native user message' : `native first input unconfirmed: ${result.reason}`);
@@ -2239,6 +2278,41 @@ export class TeamsMachine extends MachineCore {
2239
2278
  return filed?.code === 0 ? null : `the line was typed but its sender was not recorded: ${filed?.text ?? 'the filing door answered nothing'}`;
2240
2279
  }
2241
2280
 
2281
+ /**
2282
+ * A fresh Windows Codex's first input, pasted into its empty composer and submitted once: the input its command
2283
+ * line could not carry. Each bracketed paste stays under CODEX_WINDOWS_PASTE_CHARS, and Codex joins consecutive
2284
+ * ones into one entry. Delivery is not claimed here — the caller's rollout receipt confirms what Codex took.
2285
+ */
2286
+ async #typeCodexFirstInput(session, text, signal) {
2287
+ const body = String(text).replace(/\r\n?/g, '\n').replace(/\n+$/u, '');
2288
+ if (!body.trim() || /[\u0000-\u0008\u000b-\u001f\u007f]/u.test(body)) return { delivered: false, reason: 'unsupported-text' };
2289
+ if (this.inputInFlight.has(session.id)) return { delivered: false, reason: 'input-in-flight' };
2290
+ this.inputInFlight.add(session.id);
2291
+ try {
2292
+ const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2293
+ // Styled, so the composer's dim placeholder ("Ask Codex to do anything") reads as empty.
2294
+ const composer = async () => terminal.composerText?.('codex', await this.host.capture(session.id, { preserveEscapes: true })) ?? null;
2295
+ if (await composer() !== '') return { delivered: false, reason: 'composer-not-empty' };
2296
+ for (const paste of codexWindowsPastes(body)) {
2297
+ if (signal.aborted) return { delivered: false, reason: 'aborted' };
2298
+ await this.host.sendInput(session.id, paste, { submitMode: 'none' });
2299
+ await pause(300);
2300
+ }
2301
+ let held = false;
2302
+ for (let attempt = 0; attempt < 20 && !held && !signal.aborted; attempt += 1) {
2303
+ held = Boolean(await composer());
2304
+ if (!held) await pause(150);
2305
+ }
2306
+ if (!held) return { delivered: false, reason: 'paste-unconfirmed' };
2307
+ await this.host.sendInput(session.id, '\r', { submitMode: 'single' });
2308
+ return { delivered: true };
2309
+ } catch (error) {
2310
+ return { delivered: false, reason: error?.message ?? String(error) };
2311
+ } finally {
2312
+ this.inputInFlight.delete(session.id);
2313
+ }
2314
+ }
2315
+
2242
2316
  async #deliverPaneText(session, contextKey, text, options = {}) {
2243
2317
  const kind = this.#paneKind(contextKey, session);
2244
2318
  if (kind === 'codex' && codexUpdatePrompt(await this.host.capture(session.id, { lines: 120 }))) return { delivered: false, reason: 'codex-update-prompt: delivery refused; restart Codex with check_for_update_on_startup=false' };
@@ -7,6 +7,9 @@ import { codexQuestions } from './native-questions.mjs';
7
7
 
8
8
  const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9
9
 
10
+ /** Input as a composer takes it: line ends as `\n`, without the outer whitespace a submit trims. */
11
+ const normalizedInput = (text) => String(text).replace(/\r\n?/g, '\n').trim();
12
+
10
13
  /** Rollouts one daemon's launches have already taken as their receipt, with when: a launch that began before a
11
14
  * claim does not answer from that transcript, while a later launch (a resume of the same session) still can, since
12
15
  * its receipt must be newer than it anyway. This orders launches; it does not prove ownership — two panes in one
@@ -100,7 +103,10 @@ export async function confirmNativeInitialInput({ binding, kind, text, startedAt
100
103
  content.every((part) => ['text', 'input_text'].includes(part.type)) ? content.map((part) => part.text).join('') : null;
101
104
  // The transcript that confirms the input also names the session it belongs to: the launch records it,
102
105
  // so a session can be told apart as one an agent launched (Claude Code's sessionId; Codex's rollout id).
103
- if (value === text) {
106
+ // A Windows Codex input may have been pasted: its line ends normalized and trailing newlines dropped.
107
+ const same = codexSessions && typeof value === 'string'
108
+ ? normalizedInput(value) === normalizedInput(text) : value === text;
109
+ if (same) {
104
110
  const native = kind === 'claude-code' ? record.sessionId : /([0-9a-f]{8}-[0-9a-f-]{27})\.jsonl$/.exec(entry.path)?.[1];
105
111
  if (codexSessions) claimedRollouts.set(entry.path, Date.now());
106
112
  return { delivered: true, receipt: 'native-user-message', ...(typeof native === 'string' && native ? { native_session: native } : {}) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.106",
3
+ "version": "0.3.107",
4
4
  "type": "module",
5
5
  "description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
6
6
  "license": "MIT",
package/terminal-mail.mjs CHANGED
@@ -5,7 +5,7 @@ import { join } from 'node:path';
5
5
  import { createHash, randomUUID } from 'node:crypto';
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { StringDecoder } from 'node:string_decoder';
8
- import { codexUpdatePrompt } from './codex-startup.mjs';
8
+ import { codexUpdatePrompt, codexWindowsPastes } from './codex-startup.mjs';
9
9
 
10
10
  // When process `pid` started (ms), from `ps -o etime=` ([[dd-]hh:]mm:ss); null when it cannot be read.
11
11
  function startedAt(pid) {
@@ -154,8 +154,14 @@ export class TerminalMailbox {
154
154
  // it never guesses that an unacknowledged paste did not happen.
155
155
  Object.assign(batch, { phase: 'paste_started', terminal: this.session.id, pid: this.session.rootPid ?? null, pasted_at: Date.now() }); this.save();
156
156
  // The terminal host's final newline selects one bracketed paste;
157
- // submitMode:none holds the separate Enter until the paste completes.
158
- await this.host.sendInput(this.session.id, `${batch.text}\n`, { submitMode: 'none' });
157
+ // submitMode:none holds the separate Enter until the paste completes. A Windows Codex takes its own pastes:
158
+ // at most 8000 characters each, non-ASCII as key events (codex-startup.mjs codexWindowsPastes).
159
+ if (process.platform === 'win32' && batch.kind === 'codex') {
160
+ for (const paste of codexWindowsPastes(batch.text)) {
161
+ await this.host.sendInput(this.session.id, paste, { submitMode: 'none' });
162
+ await new Promise((resolve) => setTimeout(resolve, 300));
163
+ }
164
+ } else await this.host.sendInput(this.session.id, `${batch.text}\n`, { submitMode: 'none' });
159
165
  batch.phase = 'pasted'; this.save();
160
166
  }
161
167
  if (batch.phase === 'paste_started') {