farming-code 2.2.8 → 2.2.12

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.
Files changed (107) hide show
  1. package/README.md +42 -28
  2. package/README.zh_cn.md +42 -28
  3. package/THIRD_PARTY_NOTICES.md +11 -2
  4. package/backend/acp-checkpoint-store.js +198 -0
  5. package/backend/acp-runtime.js +306 -83
  6. package/backend/acp-session-state.js +202 -6
  7. package/backend/acp-transcript.js +112 -0
  8. package/backend/agent-activity.js +6 -157
  9. package/backend/agent-manager.js +1592 -741
  10. package/backend/agent-provider-session.js +17 -242
  11. package/backend/agent-runtime-binding.js +219 -0
  12. package/backend/agent-session-history.js +66 -1
  13. package/backend/auth.js +79 -6
  14. package/backend/codex-models.js +81 -84
  15. package/backend/codex-session-archive.js +54 -0
  16. package/backend/codex-terminal-profile.js +500 -0
  17. package/backend/codex-transcript-sanitizer.js +12 -0
  18. package/backend/codex-transcript.js +230 -8
  19. package/backend/config-manager.js +35 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/executable-discovery.js +2 -0
  22. package/backend/farming-net-pass.js +285 -0
  23. package/backend/farming-net-registry.js +112 -0
  24. package/backend/farming-net-server.js +298 -0
  25. package/backend/farming-session-store.js +5 -13
  26. package/backend/git-worktree-info.js +181 -0
  27. package/backend/local-session-engine.js +411 -186
  28. package/backend/main-page-session.js +5 -2
  29. package/backend/native-pty-controller-generation.js +106 -0
  30. package/backend/native-pty-host-client.js +275 -7
  31. package/backend/native-pty-host-identity.js +86 -0
  32. package/backend/native-pty-host.js +813 -114
  33. package/backend/native-session-engine.js +100 -28
  34. package/backend/packaged-node-pty.js +22 -2
  35. package/backend/provider-adapters.js +253 -0
  36. package/backend/provider-session-service.js +241 -0
  37. package/backend/runtime-observation.js +81 -0
  38. package/backend/server.js +354 -84
  39. package/backend/session-engine-bridge.js +21 -2
  40. package/backend/session-engine-router.js +1 -1
  41. package/backend/session-engine.js +1 -1
  42. package/backend/session-stream-protocol.js +185 -0
  43. package/backend/storage-layout.js +55 -0
  44. package/backend/terminal-attach-checkpoint.js +74 -0
  45. package/backend/terminal-exit-quiescence.js +39 -0
  46. package/backend/terminal-reducer-flow-control.js +97 -0
  47. package/backend/terminal-screen-state.js +11 -2
  48. package/backend/terminal-screen-worker-pool.js +59 -6
  49. package/backend/terminal-screen-worker-thread.js +97 -57
  50. package/backend/terminal-screen-worker.js +133 -51
  51. package/backend/terminal-state-serialization.js +127 -0
  52. package/backend/terminal-status.js +23 -4
  53. package/backend/usage-monitor.js +176 -17
  54. package/backend/workspace-directory.js +232 -0
  55. package/backend/workspace-file-router.js +182 -76
  56. package/backend/workspace-file-service.js +319 -4
  57. package/backend/workspace-root-registry.js +164 -0
  58. package/dist/assets/App-DWsa7DXG.js +206 -0
  59. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview-UlVd0O4D.js} +92 -92
  60. package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
  61. package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
  62. package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +12 -0
  63. package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
  64. package/dist/assets/code-dark-DPiuyeSp.css +1 -0
  65. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  66. package/dist/assets/{index-BrbljRqn.js → index-CUFPJoa5.js} +3 -3
  67. package/dist/assets/main-Bqtb5kuz.css +1 -0
  68. package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
  69. package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
  70. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  71. package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
  72. package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
  73. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  74. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  75. package/dist/farming-2/site.webmanifest +2 -0
  76. package/dist/index.html +1 -1
  77. package/frontend/farming-net/app.css +625 -0
  78. package/frontend/farming-net/app.js +268 -0
  79. package/frontend/farming-net/index.html +86 -0
  80. package/frontend/reading-anchor.js +198 -0
  81. package/frontend/session-bridge.js +12 -3
  82. package/frontend/session-modal-bridge.js +5 -12
  83. package/frontend/skins/crt/app.js +1978 -793
  84. package/frontend/skins/crt/index.html +313 -23
  85. package/frontend/skins/crt/styles/billing.css +294 -223
  86. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  87. package/frontend/terminal-replay.js +372 -0
  88. package/package.json +10 -3
  89. package/shared/browser-protocol.d.ts +5 -0
  90. package/shared/browser-protocol.js +130 -0
  91. package/dist/assets/App-8dYAM6ql.js +0 -124
  92. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  93. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  94. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  95. package/dist/assets/ReviewPage-BaXu1ZdX.js +0 -3
  96. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  97. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  98. package/dist/assets/main-D073SnW4.css +0 -1
  99. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  100. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  101. package/dist/assets/qoder-gHCinseV.svg +0 -1
  102. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  103. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  104. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  105. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  106. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  107. /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
@@ -1,6 +1,8 @@
1
1
  const TerminalScreenWorker = require('./terminal-screen-worker');
2
2
 
3
3
  const DEFAULT_POOL_SIZE = 3;
4
+ const DEFAULT_RETRY_DELAY_MS = 250;
5
+ const MAX_RETRY_DELAY_MS = 5000;
4
6
 
5
7
  function normalizePoolSize(value) {
6
8
  const parsed = Number(value);
@@ -21,6 +23,11 @@ class TerminalScreenWorkerPool {
21
23
  this.waiters = [];
22
24
  this.readyWaiters = [];
23
25
  this.pendingStarts = 0;
26
+ this.consecutiveStartFailures = 0;
27
+ this.retryDelayMs = Number.isFinite(options.retryDelayMs)
28
+ ? Math.max(0, Number(options.retryDelayMs))
29
+ : DEFAULT_RETRY_DELAY_MS;
30
+ this.retryTimer = null;
24
31
  this.disposed = false;
25
32
 
26
33
  this.ensureCapacity();
@@ -32,6 +39,7 @@ class TerminalScreenWorkerPool {
32
39
  idle: this.idle.length,
33
40
  pendingStarts: this.pendingStarts,
34
41
  waiters: this.waiters.length,
42
+ consecutiveStartFailures: this.consecutiveStartFailures,
35
43
  };
36
44
  }
37
45
 
@@ -60,7 +68,7 @@ class TerminalScreenWorkerPool {
60
68
  const worker = this.idle.shift();
61
69
  if (worker) {
62
70
  this.ensureCapacity();
63
- return this.prepareWorker(worker, options);
71
+ return this.prepareCheckedWorker(worker, options);
64
72
  }
65
73
 
66
74
  return new Promise((resolve, reject) => {
@@ -70,7 +78,7 @@ class TerminalScreenWorkerPool {
70
78
  }
71
79
 
72
80
  ensureCapacity() {
73
- if (this.disposed || this.size <= 0) {
81
+ if (this.disposed || this.size <= 0 || this.retryTimer) {
74
82
  return;
75
83
  }
76
84
 
@@ -82,9 +90,11 @@ class TerminalScreenWorkerPool {
82
90
 
83
91
  startWorker() {
84
92
  this.pendingStarts += 1;
93
+ let failed = false;
85
94
  this.createReadyWorker()
86
95
  .then((worker) => {
87
96
  this.pendingStarts -= 1;
97
+ this.consecutiveStartFailures = 0;
88
98
  if (this.disposed) {
89
99
  worker.dispose().catch(() => {});
90
100
  return;
@@ -92,18 +102,41 @@ class TerminalScreenWorkerPool {
92
102
  this.deliverWorker(worker);
93
103
  })
94
104
  .catch((error) => {
105
+ failed = true;
95
106
  this.pendingStarts -= 1;
107
+ this.consecutiveStartFailures += 1;
96
108
  const waiter = this.waiters.shift();
97
109
  if (waiter) {
98
110
  waiter.reject(error);
99
111
  }
112
+ if (this.pendingStarts === 0 && this.idle.length < this.size) {
113
+ const readyWaiters = this.readyWaiters.splice(0);
114
+ readyWaiters.forEach(({ reject }) => reject(error));
115
+ }
100
116
  })
101
117
  .finally(() => {
102
118
  this.notifyReadyWaiters();
103
- this.ensureCapacity();
119
+ if (failed) {
120
+ this.scheduleCapacityRetry();
121
+ } else {
122
+ this.ensureCapacity();
123
+ }
104
124
  });
105
125
  }
106
126
 
127
+ scheduleCapacityRetry() {
128
+ if (this.disposed || this.size <= 0 || this.retryTimer) return;
129
+ const delayMs = Math.min(
130
+ MAX_RETRY_DELAY_MS,
131
+ this.retryDelayMs * Math.max(1, 2 ** Math.min(this.consecutiveStartFailures - 1, 5)),
132
+ );
133
+ this.retryTimer = setTimeout(() => {
134
+ this.retryTimer = null;
135
+ this.ensureCapacity();
136
+ }, delayMs);
137
+ this.retryTimer.unref?.();
138
+ }
139
+
107
140
  async createReadyWorker() {
108
141
  const worker = new this.WorkerClass(this.workerOptions);
109
142
  try {
@@ -122,12 +155,28 @@ class TerminalScreenWorkerPool {
122
155
  async prepareWorker(worker, options = {}) {
123
156
  const cols = Number(options.cols || this.workerOptions.cols || 80);
124
157
  const rows = Number(options.rows || this.workerOptions.rows || 30);
125
- if (Number.isFinite(cols) && cols > 0 && Number.isFinite(rows) && rows > 0) {
126
- await worker.resize(cols, rows);
158
+ if (typeof options.runtimeEpoch === 'string') {
159
+ await worker.setRuntimeEpoch(options.runtimeEpoch, cols, rows);
160
+ } else if (Number.isFinite(cols) && cols > 0 && Number.isFinite(rows) && rows > 0) {
161
+ await worker.resize(cols, rows, 1);
127
162
  }
128
163
  return worker;
129
164
  }
130
165
 
166
+ async prepareCheckedWorker(worker, options = {}) {
167
+ try {
168
+ return await this.prepareWorker(worker, options);
169
+ } catch (error) {
170
+ try {
171
+ await worker.dispose();
172
+ } catch {
173
+ // best effort
174
+ }
175
+ this.ensureCapacity();
176
+ throw error;
177
+ }
178
+ }
179
+
131
180
  deliverWorker(worker) {
132
181
  const waiter = this.waiters.shift();
133
182
  if (!waiter) {
@@ -136,7 +185,7 @@ class TerminalScreenWorkerPool {
136
185
  return;
137
186
  }
138
187
 
139
- this.prepareWorker(worker, waiter.options).then(waiter.resolve, waiter.reject);
188
+ this.prepareCheckedWorker(worker, waiter.options).then(waiter.resolve, waiter.reject);
140
189
  }
141
190
 
142
191
  notifyReadyWaiters() {
@@ -153,6 +202,10 @@ class TerminalScreenWorkerPool {
153
202
  }
154
203
 
155
204
  this.disposed = true;
205
+ if (this.retryTimer) {
206
+ clearTimeout(this.retryTimer);
207
+ this.retryTimer = null;
208
+ }
156
209
  const error = new Error('Terminal screen worker pool is disposed');
157
210
  this.waiters.splice(0).forEach(({ reject }) => reject(error));
158
211
  this.readyWaiters.splice(0).forEach(({ reject }) => reject(error));
@@ -1,20 +1,17 @@
1
1
  const { parentPort, workerData } = require('worker_threads');
2
2
  const TerminalScreenState = require('./terminal-screen-state');
3
3
 
4
- const PREVIEW_FLUSH_INTERVAL_MS = 120;
5
- const MAX_PENDING_DATA_BYTES = 128 * 1024;
4
+ const PREVIEW_FLUSH_INTERVAL_MS = 50;
6
5
 
7
6
  const screenState = new TerminalScreenState(workerData || {});
8
- let pendingData = '';
9
- let previewFlushTimer = null;
7
+ let runtimeEpoch = typeof workerData.runtimeEpoch === 'string' ? workerData.runtimeEpoch : '';
8
+ let appliedOutputSeq = 0;
9
+ let appliedStateRevision = 0;
10
10
  let lastPreviewText = '';
11
11
  let lastTitle = '';
12
12
  let lastSnapshotFingerprint = '';
13
13
  let messageQueue = Promise.resolve();
14
-
15
- function byteLength(value) {
16
- return Buffer.byteLength(String(value || ''), 'utf8');
17
- }
14
+ let previewFlushTimer = null;
18
15
 
19
16
  function postPreview(state) {
20
17
  const previewText = state.previewText || '';
@@ -39,86 +36,129 @@ function postPreview(state) {
39
36
  });
40
37
  }
41
38
 
42
- async function flushPending(options = {}) {
43
- if (previewFlushTimer) {
44
- clearTimeout(previewFlushTimer);
45
- previewFlushTimer = null;
46
- }
47
-
48
- if (pendingData) {
49
- const data = pendingData;
50
- pendingData = '';
51
- const state = await screenState.write(data);
52
- if (options.emitPreview !== false) {
53
- postPreview(state);
54
- }
55
- return screenState.getState({ includeRenderOutput: options.includeRenderOutput });
56
- }
39
+ function withOutputSeq(state) {
40
+ return {
41
+ ...state,
42
+ runtimeEpoch,
43
+ outputSeq: appliedOutputSeq,
44
+ stateRevision: appliedStateRevision,
45
+ };
46
+ }
57
47
 
58
- const state = screenState.getState({ includeRenderOutput: options.includeRenderOutput });
48
+ function currentState(options = {}) {
49
+ const state = screenState.getState({
50
+ includeRenderOutput: options.includeRenderOutput,
51
+ refreshPreview: options.refreshPreview,
52
+ });
59
53
  if (options.emitPreview) {
60
54
  postPreview(state);
61
55
  }
62
- return state;
56
+ return withOutputSeq(state);
63
57
  }
64
58
 
65
- function scheduleFlush() {
66
- if (previewFlushTimer) {
67
- return;
68
- }
69
-
59
+ function schedulePreview() {
60
+ if (previewFlushTimer) return;
70
61
  previewFlushTimer = setTimeout(() => {
71
- flushPending({ includeRenderOutput: false, emitPreview: true }).catch((error) => {
72
- parentPort.postMessage({
73
- type: 'error',
74
- message: error.message,
75
- });
76
- });
62
+ previewFlushTimer = null;
63
+ postPreview(currentState({
64
+ includeRenderOutput: false,
65
+ refreshPreview: true,
66
+ }));
77
67
  }, PREVIEW_FLUSH_INTERVAL_MS);
68
+ if (typeof previewFlushTimer.unref === 'function') previewFlushTimer.unref();
78
69
  }
79
70
 
80
- async function appendData(data) {
81
- const text = String(data || '');
82
- if (!text) return;
83
-
84
- if (pendingData && byteLength(pendingData) + byteLength(text) > MAX_PENDING_DATA_BYTES) {
85
- await flushPending({ includeRenderOutput: false, emitPreview: false });
71
+ function assertNextRevision(stateRevision, transition) {
72
+ if (!Number.isFinite(stateRevision) || stateRevision !== appliedStateRevision + 1) {
73
+ throw new Error(
74
+ `Terminal screen ${transition} revision gap: expected ${appliedStateRevision + 1}, received ${stateRevision}`,
75
+ );
86
76
  }
77
+ }
87
78
 
88
- if (byteLength(text) > MAX_PENDING_DATA_BYTES) {
89
- const state = await screenState.write(text);
90
- postPreview(state);
91
- return;
79
+ async function appendEntries(rawEntries) {
80
+ const entries = Array.isArray(rawEntries) ? rawEntries : [];
81
+ if (entries.length === 0) return;
82
+
83
+ let expectedRevision = appliedStateRevision + 1;
84
+ let nextOutputSeq = appliedOutputSeq;
85
+ let data = '';
86
+ for (const entry of entries) {
87
+ const stateRevision = Number(entry && entry.stateRevision);
88
+ if (!Number.isFinite(stateRevision) || stateRevision !== expectedRevision) {
89
+ throw new Error(
90
+ `Terminal screen append revision gap: expected ${expectedRevision}, received ${entry && entry.stateRevision}`,
91
+ );
92
+ }
93
+ const text = String((entry && entry.data) || '');
94
+ if (!text) {
95
+ throw new Error(`Terminal screen append revision ${stateRevision} has no data`);
96
+ }
97
+ const outputSeq = Number(entry && entry.outputSeq);
98
+ if (!Number.isFinite(outputSeq) || outputSeq !== nextOutputSeq + 1) {
99
+ throw new Error(
100
+ `Terminal screen output sequence gap: expected ${nextOutputSeq + 1}, received ${entry && entry.outputSeq}`,
101
+ );
102
+ }
103
+ data += text;
104
+ nextOutputSeq = outputSeq;
105
+ expectedRevision += 1;
92
106
  }
93
107
 
94
- pendingData += text;
95
- scheduleFlush();
108
+ const state = await screenState.write(data);
109
+ appliedStateRevision = expectedRevision - 1;
110
+ appliedOutputSeq = nextOutputSeq;
111
+ schedulePreview();
112
+ return state;
96
113
  }
97
114
 
98
115
  async function handleRequest(message) {
99
116
  switch (message.type) {
100
117
  case 'append':
101
- await appendData(message.data);
102
- return null;
118
+ await appendEntries(message.entries);
119
+ return currentState({
120
+ includeRenderOutput: false,
121
+ refreshPreview: false,
122
+ });
123
+ case 'set-runtime-epoch':
124
+ runtimeEpoch = typeof message.runtimeEpoch === 'string' ? message.runtimeEpoch : '';
125
+ appliedOutputSeq = 0;
126
+ appliedStateRevision = 0;
127
+ if (Number.isFinite(message.cols) && Number.isFinite(message.rows)) {
128
+ screenState.resize(message.cols, message.rows);
129
+ }
130
+ return currentState({ includeRenderOutput: false });
103
131
  case 'resize': {
104
- await flushPending({ includeRenderOutput: false, emitPreview: false });
105
- const state = screenState.resize(message.cols, message.rows);
132
+ assertNextRevision(message.stateRevision, 'resize');
133
+ screenState.resize(message.cols, message.rows);
134
+ appliedStateRevision = message.stateRevision;
135
+ const state = screenState.getState({ includeRenderOutput: true });
106
136
  postPreview(state);
107
- return state;
137
+ return withOutputSeq(state);
108
138
  }
109
139
  case 'clear': {
110
- await flushPending({ includeRenderOutput: false, emitPreview: false });
140
+ assertNextRevision(message.stateRevision, 'clear');
111
141
  const state = await screenState.clearBuffer();
142
+ const outputSeq = Number(message.outputSeq);
143
+ if (!Number.isFinite(outputSeq) || outputSeq !== appliedOutputSeq) {
144
+ throw new Error(
145
+ `Terminal screen clear output sequence mismatch: expected ${appliedOutputSeq}, received ${message.outputSeq}`,
146
+ );
147
+ }
148
+ appliedStateRevision = message.stateRevision;
112
149
  postPreview(state);
113
- return state;
150
+ return withOutputSeq(state);
114
151
  }
115
152
  case 'get-state':
116
- return flushPending({
153
+ return currentState({
117
154
  includeRenderOutput: message.includeRenderOutput !== false,
118
155
  emitPreview: false,
119
156
  });
120
157
  case 'dispose':
121
- await flushPending({ includeRenderOutput: false, emitPreview: false });
158
+ if (previewFlushTimer) {
159
+ clearTimeout(previewFlushTimer);
160
+ previewFlushTimer = null;
161
+ }
122
162
  screenState.dispose();
123
163
  return { disposed: true };
124
164
  default:
@@ -6,6 +6,7 @@ const { Worker } = require('worker_threads');
6
6
  const APPEND_FLUSH_INTERVAL_MS = 16;
7
7
  const MAX_PENDING_APPEND_BYTES = 128 * 1024;
8
8
  const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
9
+ const DEFAULT_STATE_REQUEST_HARD_TIMEOUT_MS = 5000;
9
10
  const PACKAGED_WORKER_FILE = 'terminal-screen-worker-thread.pkg.js';
10
11
  const SOURCE_WORKER_FILE = 'terminal-screen-worker-thread.js';
11
12
 
@@ -28,11 +29,17 @@ class TerminalScreenWorker extends EventEmitter {
28
29
  super();
29
30
  this.nextRequestId = 1;
30
31
  this.pendingRequests = new Map();
31
- this.pendingAppendData = '';
32
+ this.pendingAppendEntries = [];
33
+ this.pendingAppendBytes = 0;
34
+ this.pendingAppendWaiters = [];
32
35
  this.appendFlushTimer = null;
36
+ this.stateRequestInFlight = null;
33
37
  this.requestTimeoutMs = Number.isFinite(options.requestTimeoutMs)
34
38
  ? Math.max(1, Math.floor(options.requestTimeoutMs))
35
39
  : DEFAULT_REQUEST_TIMEOUT_MS;
40
+ this.stateRequestHardTimeoutMs = Number.isFinite(options.stateRequestHardTimeoutMs)
41
+ ? Math.max(1, Math.floor(options.stateRequestHardTimeoutMs))
42
+ : Math.min(this.requestTimeoutMs, DEFAULT_STATE_REQUEST_HARD_TIMEOUT_MS);
36
43
  this.failed = false;
37
44
  this.disposed = false;
38
45
  const workerFile = resolveWorkerFile();
@@ -40,6 +47,7 @@ class TerminalScreenWorker extends EventEmitter {
40
47
  const workerData = { ...options };
41
48
  delete workerData.WorkerClass;
42
49
  delete workerData.requestTimeoutMs;
50
+ delete workerData.stateRequestHardTimeoutMs;
43
51
  this.worker = new WorkerClass(path.join(__dirname, workerFile), {
44
52
  workerData,
45
53
  });
@@ -79,7 +87,7 @@ class TerminalScreenWorker extends EventEmitter {
79
87
  });
80
88
 
81
89
  this.worker.on('error', (error) => {
82
- this.emit('error', error);
90
+ this.handleWorkerFailure(error);
83
91
  });
84
92
 
85
93
  this.worker.on('exit', (code) => {
@@ -87,26 +95,34 @@ class TerminalScreenWorker extends EventEmitter {
87
95
  return;
88
96
  }
89
97
 
90
- const error = new Error(`Terminal screen worker exited unexpectedly with code ${code}`);
91
- this.failed = true;
92
- this.pendingRequests.forEach(({ reject, timer }) => {
93
- clearTimeout(timer);
94
- reject(error);
95
- });
96
- this.pendingRequests.clear();
97
- this.emit('error', error);
98
+ this.handleWorkerFailure(new Error(`Terminal screen worker exited unexpectedly with code ${code}`));
98
99
  });
99
100
  }
100
101
 
101
- handlePostMessageFailure(error) {
102
+ handleWorkerFailure(error) {
102
103
  if (this.disposed) return;
104
+ const failure = error instanceof Error ? error : new Error(String(error));
105
+ const shouldEmit = !this.failed;
103
106
  this.failed = true;
104
107
  if (this.appendFlushTimer) {
105
108
  clearTimeout(this.appendFlushTimer);
106
109
  this.appendFlushTimer = null;
107
110
  }
108
- this.pendingAppendData = '';
109
- this.emit('error', error instanceof Error ? error : new Error(String(error)));
111
+ this.pendingAppendEntries = [];
112
+ this.pendingAppendBytes = 0;
113
+ this.pendingAppendWaiters.splice(0).forEach(({ reject }) => reject(failure));
114
+ this.pendingRequests.forEach(({ reject, timer }) => {
115
+ clearTimeout(timer);
116
+ reject(failure);
117
+ });
118
+ this.pendingRequests.clear();
119
+ if (shouldEmit && this.listenerCount('error') > 0) {
120
+ this.emit('error', failure);
121
+ }
122
+ }
123
+
124
+ handlePostMessageFailure(error) {
125
+ this.handleWorkerFailure(error);
110
126
  }
111
127
 
112
128
  postWorkerMessage(message, options = {}) {
@@ -125,20 +141,24 @@ class TerminalScreenWorker extends EventEmitter {
125
141
  clearTimeout(this.appendFlushTimer);
126
142
  this.appendFlushTimer = null;
127
143
  }
128
- if (this.disposed || this.failed || !this.pendingAppendData) {
129
- this.pendingAppendData = '';
144
+ if (this.disposed || this.failed || this.pendingAppendEntries.length === 0) {
145
+ this.pendingAppendEntries = [];
146
+ this.pendingAppendBytes = 0;
130
147
  return;
131
148
  }
132
149
 
133
- const data = this.pendingAppendData;
134
- this.pendingAppendData = '';
135
- this.postWorkerMessage({
136
- type: 'append',
137
- data,
138
- });
150
+ const entries = this.pendingAppendEntries;
151
+ const waiters = this.pendingAppendWaiters;
152
+ this.pendingAppendEntries = [];
153
+ this.pendingAppendBytes = 0;
154
+ this.pendingAppendWaiters = [];
155
+ this.request('append', { entries }, { flushAppend: false }).then(
156
+ state => waiters.forEach(({ resolve }) => resolve(state)),
157
+ error => waiters.forEach(({ reject }) => reject(error)),
158
+ );
139
159
  }
140
160
 
141
- request(type, payload = {}) {
161
+ request(type, payload = {}, options = {}) {
142
162
  if (this.disposed) {
143
163
  return Promise.reject(new Error('Terminal screen worker is disposed'));
144
164
  }
@@ -146,7 +166,9 @@ class TerminalScreenWorker extends EventEmitter {
146
166
  return Promise.reject(new Error('Terminal screen worker is not available'));
147
167
  }
148
168
 
149
- this.flushAppend();
169
+ if (options.flushAppend !== false) {
170
+ this.flushAppend();
171
+ }
150
172
  if (this.failed) {
151
173
  return Promise.reject(new Error('Terminal screen worker is not available'));
152
174
  }
@@ -154,12 +176,15 @@ class TerminalScreenWorker extends EventEmitter {
154
176
  this.nextRequestId += 1;
155
177
 
156
178
  return new Promise((resolve, reject) => {
179
+ const requestTimeoutMs = Number.isFinite(options.timeoutMs)
180
+ ? Math.max(1, Math.floor(options.timeoutMs))
181
+ : this.requestTimeoutMs;
157
182
  const timer = setTimeout(() => {
158
183
  this.pendingRequests.delete(requestId);
159
184
  const error = new Error(`Terminal screen worker request timed out: ${type}`);
160
185
  error.code = 'ETIMEDOUT';
161
186
  reject(error);
162
- }, this.requestTimeoutMs);
187
+ }, requestTimeoutMs);
163
188
  if (typeof timer.unref === 'function') timer.unref();
164
189
 
165
190
  this.pendingRequests.set(requestId, { resolve, reject, timer });
@@ -172,56 +197,108 @@ class TerminalScreenWorker extends EventEmitter {
172
197
  } catch (error) {
173
198
  this.pendingRequests.delete(requestId);
174
199
  clearTimeout(timer);
175
- this.failed = true;
200
+ this.handlePostMessageFailure(error);
176
201
  reject(error);
177
202
  }
178
203
  });
179
204
  }
180
205
 
181
- append(data) {
206
+ append(data, stateRevision, outputSeq = null) {
182
207
  if (this.disposed || this.failed) {
183
- return;
208
+ return Promise.reject(new Error('Terminal screen worker is not available'));
184
209
  }
185
210
 
186
211
  const text = String(data || '');
187
212
  if (!text) return;
213
+ if (!Number.isFinite(stateRevision)) {
214
+ this.handlePostMessageFailure(new Error('Terminal screen append requires a finite state revision'));
215
+ return Promise.reject(new Error('Terminal screen append requires a finite state revision'));
216
+ }
188
217
 
189
218
  if (
190
- this.pendingAppendData &&
191
- byteLength(this.pendingAppendData) + byteLength(text) > MAX_PENDING_APPEND_BYTES
219
+ this.pendingAppendEntries.length > 0 &&
220
+ this.pendingAppendBytes + byteLength(text) > MAX_PENDING_APPEND_BYTES
192
221
  ) {
193
222
  this.flushAppend();
194
223
  }
195
224
 
196
- if (byteLength(text) > MAX_PENDING_APPEND_BYTES) {
197
- this.postWorkerMessage({
198
- type: 'append',
199
- data: text,
200
- });
201
- return;
202
- }
203
-
204
- this.pendingAppendData += text;
205
- if (this.appendFlushTimer) {
206
- return;
207
- }
225
+ return new Promise((resolve, reject) => {
226
+ this.pendingAppendEntries.push({ data: text, stateRevision, outputSeq });
227
+ this.pendingAppendBytes += byteLength(text);
228
+ this.pendingAppendWaiters.push({ resolve, reject });
229
+ if (byteLength(text) > MAX_PENDING_APPEND_BYTES) {
230
+ this.flushAppend();
231
+ return;
232
+ }
233
+ if (this.appendFlushTimer) return;
234
+ this.appendFlushTimer = setTimeout(() => {
235
+ this.flushAppend();
236
+ }, APPEND_FLUSH_INTERVAL_MS);
237
+ });
238
+ }
208
239
 
209
- this.appendFlushTimer = setTimeout(() => {
210
- this.flushAppend();
211
- }, APPEND_FLUSH_INTERVAL_MS);
240
+ resize(cols, rows, stateRevision) {
241
+ return this.request('resize', { cols, rows, stateRevision });
212
242
  }
213
243
 
214
- resize(cols, rows) {
215
- return this.request('resize', { cols, rows });
244
+ setRuntimeEpoch(runtimeEpoch, cols, rows) {
245
+ return this.request('set-runtime-epoch', { runtimeEpoch, cols, rows });
216
246
  }
217
247
 
218
- clear() {
219
- return this.request('clear');
248
+ clear(stateRevision, outputSeq = null) {
249
+ return this.request('clear', { stateRevision, outputSeq });
220
250
  }
221
251
 
222
252
  getState(options = {}) {
223
- return this.request('get-state', {
224
- includeRenderOutput: options.includeRenderOutput !== false,
253
+ if (!this.stateRequestInFlight) {
254
+ const request = this.request('get-state', {
255
+ // A full checkpoint can satisfy callers that only need metadata too,
256
+ // while the reverse would make coalescing unsafe.
257
+ includeRenderOutput: true,
258
+ }, {
259
+ // Caller deadlines are deliberately softer than this shared deadline.
260
+ // A timed-out caller may stop waiting, but the single-flight itself
261
+ // must never poison every later checkpoint for the generic 30s worker
262
+ // request timeout. Crossing this hard deadline means the authoritative
263
+ // reducer can no longer prove progress, so fail it closed.
264
+ timeoutMs: this.stateRequestHardTimeoutMs,
265
+ });
266
+ const sharedRequest = request.catch((error) => {
267
+ if (error && error.code === 'ETIMEDOUT') {
268
+ this.handleWorkerFailure(error);
269
+ }
270
+ throw error;
271
+ }).finally(() => {
272
+ if (this.stateRequestInFlight === sharedRequest) {
273
+ this.stateRequestInFlight = null;
274
+ }
275
+ });
276
+ this.stateRequestInFlight = sharedRequest;
277
+ }
278
+
279
+ const sharedRequest = this.stateRequestInFlight;
280
+ const timeoutMs = Number.isFinite(options.timeoutMs)
281
+ ? Math.max(1, Math.floor(options.timeoutMs))
282
+ : null;
283
+ if (timeoutMs === null) return sharedRequest;
284
+
285
+ return new Promise((resolve, reject) => {
286
+ const timer = setTimeout(() => {
287
+ const error = new Error('Terminal screen worker request timed out: get-state');
288
+ error.code = 'ETIMEDOUT';
289
+ reject(error);
290
+ }, timeoutMs);
291
+ if (typeof timer.unref === 'function') timer.unref();
292
+ sharedRequest.then(
293
+ (state) => {
294
+ clearTimeout(timer);
295
+ resolve(state);
296
+ },
297
+ (error) => {
298
+ clearTimeout(timer);
299
+ reject(error);
300
+ },
301
+ );
225
302
  });
226
303
  }
227
304
 
@@ -239,7 +316,11 @@ class TerminalScreenWorker extends EventEmitter {
239
316
  clearTimeout(this.appendFlushTimer);
240
317
  this.appendFlushTimer = null;
241
318
  }
242
- this.pendingAppendData = '';
319
+ this.pendingAppendEntries = [];
320
+ this.pendingAppendBytes = 0;
321
+ this.pendingAppendWaiters.splice(0).forEach(({ reject }) => {
322
+ reject(new Error('Terminal screen worker is disposed'));
323
+ });
243
324
  this.disposed = true;
244
325
  this.pendingRequests.forEach(({ reject, timer }) => {
245
326
  clearTimeout(timer);
@@ -253,3 +334,4 @@ class TerminalScreenWorker extends EventEmitter {
253
334
  module.exports = TerminalScreenWorker;
254
335
  module.exports.resolveWorkerFile = resolveWorkerFile;
255
336
  module.exports.DEFAULT_REQUEST_TIMEOUT_MS = DEFAULT_REQUEST_TIMEOUT_MS;
337
+ module.exports.DEFAULT_STATE_REQUEST_HARD_TIMEOUT_MS = DEFAULT_STATE_REQUEST_HARD_TIMEOUT_MS;