maka-agent 0.2.0-dev.26.20260909 → 0.2.0-dev.28.20260911

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 (125) hide show
  1. package/LICENSE +106 -0
  2. package/dist/cli-core.js +28 -0
  3. package/dist/runtime-host-cli.js +43 -3
  4. package/dist/runtime-host-handoff-surface.js +10 -8
  5. package/dist/runtime-host-lifecycle-transaction.js +28 -3
  6. package/dist/runtime-host-local-source-retirement.js +1 -1
  7. package/dist/runtime-host-run-command.js +15 -7
  8. package/dist/runtime-host-setup-command.js +29 -6
  9. package/dist/runtime-host-update-command.js +49 -5
  10. package/dist/session-export-command.js +72 -0
  11. package/dist/session-import-command.js +69 -0
  12. package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
  13. package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
  14. package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
  15. package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
  16. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  17. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +9 -4
  18. package/node_modules/@maka/core/dist/diagnostic-log.js +18 -7
  19. package/node_modules/@maka/core/dist/text-sanitize.js +8 -2
  20. package/node_modules/@maka/core/dist/usage-stats/types.js +2 -0
  21. package/node_modules/@maka/core/dist/workhub-routing.js +126 -0
  22. package/node_modules/@maka/core/package.json +2 -1
  23. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +15 -11
  24. package/node_modules/@maka/runtime/dist/archive-read-tool.js +3 -1
  25. package/node_modules/@maka/runtime/dist/bots/bot-test.js +2 -1
  26. package/node_modules/@maka/runtime/dist/bots/feishu-bridge.js +24 -20
  27. package/node_modules/@maka/runtime/dist/bots/slack-bridge.js +5 -3
  28. package/node_modules/@maka/runtime/dist/bots/wechat-bridge.js +87 -29
  29. package/node_modules/@maka/runtime/dist/bots/wecom-bridge.js +40 -36
  30. package/node_modules/@maka/runtime/dist/builtin-tools.js +21 -2
  31. package/node_modules/@maka/runtime/dist/computer-use-tools.js +4 -1
  32. package/node_modules/@maka/runtime/dist/deep-research-tools.js +11 -5
  33. package/node_modules/@maka/runtime/dist/edit-replace.js +2 -2
  34. package/node_modules/@maka/runtime/dist/filesystem-worker/operations.js +5 -6
  35. package/node_modules/@maka/runtime/dist/memory-extraction-evidence.js +7 -1
  36. package/node_modules/@maka/runtime/dist/memory-extraction.js +12 -7
  37. package/node_modules/@maka/runtime/dist/openai-responses-websocket.js +2 -5
  38. package/node_modules/@maka/runtime/dist/plugin-agent-service.js +131 -0
  39. package/node_modules/@maka/runtime/dist/plugin-approval-service.js +33 -0
  40. package/node_modules/@maka/runtime/dist/plugin-attachment-service.js +53 -0
  41. package/node_modules/@maka/runtime/dist/plugin-command-service.js +100 -0
  42. package/node_modules/@maka/runtime/dist/plugin-composition-loader.js +11 -2
  43. package/node_modules/@maka/runtime/dist/plugin-data-services.js +332 -0
  44. package/node_modules/@maka/runtime/dist/plugin-fs-service.js +62 -0
  45. package/node_modules/@maka/runtime/dist/plugin-goal-service.js +61 -0
  46. package/node_modules/@maka/runtime/dist/plugin-invocation-signal.js +24 -0
  47. package/node_modules/@maka/runtime/dist/plugin-llm-service.js +85 -0
  48. package/node_modules/@maka/runtime/dist/plugin-lsp-service.js +94 -0
  49. package/node_modules/@maka/runtime/dist/plugin-scope-registry.js +91 -0
  50. package/node_modules/@maka/runtime/dist/plugin-session-query-service.js +71 -0
  51. package/node_modules/@maka/runtime/dist/plugin-shell-env-service.js +87 -0
  52. package/node_modules/@maka/runtime/dist/plugin-shell-service.js +74 -0
  53. package/node_modules/@maka/runtime/dist/plugin-skill-service.js +93 -0
  54. package/node_modules/@maka/runtime/dist/plugin-system-prompt-service.js +346 -0
  55. package/node_modules/@maka/runtime/dist/plugin-tool-service.js +18 -52
  56. package/node_modules/@maka/runtime/dist/plugin-user-question-service.js +43 -0
  57. package/node_modules/@maka/runtime/dist/plugin-web-service.js +69 -0
  58. package/node_modules/@maka/runtime/dist/request-customization-fetch.js +14 -7
  59. package/node_modules/@maka/runtime/dist/session-export.js +178 -0
  60. package/node_modules/@maka/runtime/dist/session-import.js +100 -0
  61. package/node_modules/@maka/runtime/dist/session-manager.js +6 -1
  62. package/node_modules/@maka/runtime/dist/shell-run-manager.js +9 -6
  63. package/node_modules/@maka/runtime/dist/shell-run-tool-result.js +13 -6
  64. package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +43 -10
  65. package/node_modules/@maka/runtime/dist/stream-graph-handoff.js +6 -1
  66. package/node_modules/@maka/runtime/dist/subscription-model-fetch.js +1 -4
  67. package/node_modules/@maka/runtime/dist/system-prompt/workspace-instructions.js +11 -6
  68. package/node_modules/@maka/runtime/dist/tavily-search.js +8 -2
  69. package/node_modules/@maka/runtime/dist/text-line-window.js +49 -0
  70. package/node_modules/@maka/runtime/dist/tool-output.js +35 -12
  71. package/node_modules/@maka/runtime/dist/unified-diff.js +40 -10
  72. package/node_modules/@maka/runtime/dist/web-fetch-tool.js +4 -2
  73. package/node_modules/@maka/runtime/dist/workers/filesystem-worker.js +82 -33
  74. package/node_modules/@maka/runtime/dist/workspace-executor.js +2 -6
  75. package/node_modules/@maka/runtime/package.json +20 -1
  76. package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +73 -24
  77. package/node_modules/@maka/runtime-host/dist/client/host-handoff.js +46 -12
  78. package/node_modules/@maka/runtime-host/dist/operator/setup-frame.js +31 -21
  79. package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +7 -4
  80. package/node_modules/@maka/runtime-host/dist/protocol/artifact.js +2 -2
  81. package/node_modules/@maka/runtime-host/dist/protocol/index.js +10 -1
  82. package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +30 -5
  83. package/node_modules/@maka/runtime-host/dist/protocol/session-transcript.js +3 -2
  84. package/node_modules/@maka/runtime-host/dist/protocol/skill-catalog.js +2 -1
  85. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +25 -23
  86. package/node_modules/@maka/runtime-host/dist/server/artifact-coordinator.js +2 -0
  87. package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +62 -4
  88. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +481 -11
  89. package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +74 -0
  90. package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +6 -1
  91. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +2 -2
  92. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +30 -0
  93. package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +114 -20
  94. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +49 -12
  95. package/node_modules/@maka/runtime-host/dist/server/plugin-data-runtime.js +234 -0
  96. package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +6 -0
  97. package/node_modules/@maka/runtime-host/dist/server/plugin-platform.js +12 -0
  98. package/node_modules/@maka/runtime-host/dist/server/root-admission-owner.js +17 -8
  99. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +189 -92
  100. package/node_modules/@maka/runtime-host/dist/server/runtime-resource-projection.js +3 -9
  101. package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +20 -21
  102. package/node_modules/@maka/runtime-host/dist/server/session-transcript-pager.js +24 -2
  103. package/node_modules/@maka/runtime-host/dist/server/skill-catalog-coordinator.js +18 -2
  104. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +44 -35
  105. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +92 -10
  106. package/node_modules/@maka/runtime-host/dist/transport/framed-byte-stream-transport.js +4 -1
  107. package/node_modules/@maka/runtime-host/dist/transport/local-ipc-framing.js +4 -2
  108. package/node_modules/@maka/runtime-host/dist/transport/peer-native.js +9 -8
  109. package/node_modules/@maka/runtime-host/dist/transport/resumable-peer-stream.js +14 -9
  110. package/node_modules/@maka/storage/dist/agent-run-store.js +54 -10
  111. package/node_modules/@maka/storage/dist/context-offload-snapshot.js +32 -10
  112. package/node_modules/@maka/storage/dist/context-value-mutation-gate.js +59 -0
  113. package/node_modules/@maka/storage/dist/execution-stores.js +1 -1
  114. package/node_modules/@maka/storage/dist/session-bundle-policy.js +909 -34
  115. package/node_modules/@maka/storage/dist/session-message-projection.js +8 -4
  116. package/node_modules/@maka/storage/dist/session-store.js +2 -2
  117. package/node_modules/@maka/storage/dist/sqlite-context-offload-store.js +18 -4
  118. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +12 -5
  119. package/node_modules/@maka/storage/dist/stable-storage.js +2 -1
  120. package/node_modules/@maka/storage/package.json +2 -0
  121. package/node_modules/@modelcontextprotocol/client/dist/src-D_zzAWoS.mjs +22 -5
  122. package/node_modules/@modelcontextprotocol/client/dist/src-NAgB4Mp8.cjs +22 -5
  123. package/node_modules/zod/v4/core/memoizer.cjs +12 -16
  124. package/node_modules/zod/v4/core/memoizer.js +12 -16
  125. package/package.json +1 -1
@@ -0,0 +1,178 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ /**
20
+ * Export one Session, and the subagent subtree under it, as a portable bundle.
21
+ *
22
+ * The preparation is `exportSessionBundleState()` in `@maka/storage`, which
23
+ * already takes a consistent database snapshot, keeps writers out, filters the
24
+ * copy down to the exported Sessions, carries the context-offload closure, and
25
+ * proves the result with `PRAGMA foreign_key_check`. This module is the thin
26
+ * part: point it at a workspace, ask for a quiescent Session, and seal what
27
+ * comes back with the bundle codec.
28
+ *
29
+ * What it adds on top is the manifest — the Sessions carried, the schema
30
+ * versions the SOURCE database registers, and the connection by slug and model
31
+ * so an importer can resolve it locally. No credential is carried: the state
32
+ * allow-list in the export policy decides what is copied, and configuration is
33
+ * not on it.
34
+ */
35
+ import { mkdir, mkdtemp, rm, stat } from 'node:fs/promises';
36
+ import { join } from 'node:path';
37
+ import { tmpdir } from 'node:os';
38
+ import { SessionBundleFileError, } from '@maka/storage/session-bundle-contract';
39
+ import { createSessionBundleFileService } from '@maka/storage/session-bundle-file-service';
40
+ import { exportSessionBundleState, SessionBundleExportError, SESSION_BUNDLE_OMITTED_EVENT_TYPES, } from '@maka/storage/session-bundle-policy';
41
+ export const SESSION_EXPORT_MEDIA_TYPE = 'application/vnd.maka.session-export+json;version=1';
42
+ /** Generous by design; the codec is what enforces them. */
43
+ export const SESSION_EXPORT_BUNDLE_LIMITS = {
44
+ maxCompressedBytes: 2 * 1024 ** 3,
45
+ maxDecompressedTarBytes: 8 * 1024 ** 3,
46
+ maxPayloadBytes: 8 * 1024 ** 3,
47
+ maxFileBytes: 2 * 1024 ** 3,
48
+ maxEntryCount: 1_000_000,
49
+ maxManifestBytes: 4 * 1024 ** 2,
50
+ maxStateIdentityBytes: 4 * 1024 ** 2,
51
+ maxPathBytes: 4096,
52
+ maxPathDepth: 32,
53
+ };
54
+ export async function exportSessionBundle(input) {
55
+ if (!(await isDirectory(input.workspaceRoot))) {
56
+ return {
57
+ ok: false,
58
+ reason: { kind: 'workspace_not_found', workspaceRoot: input.workspaceRoot },
59
+ };
60
+ }
61
+ if (await exists(input.destination)) {
62
+ return { ok: false, reason: { kind: 'destination_exists' } };
63
+ }
64
+ const staging = await mkdtemp(join(tmpdir(), 'maka-session-export-'));
65
+ try {
66
+ const stateRoot = join(staging, 'state');
67
+ const workspaceRoot = join(staging, 'workspace');
68
+ // The codec seals a state tree beside a workspace tree. A Session's own
69
+ // files are the state; the user's project directory is not part of the
70
+ // conversation and is deliberately absent.
71
+ await mkdir(workspaceRoot, { recursive: true });
72
+ let plan;
73
+ try {
74
+ plan = await exportSessionBundleState({
75
+ stateRoot: input.workspaceRoot,
76
+ // Maka's desktop layout keeps state and configuration in one directory,
77
+ // so these are shared on purpose. Configuration stays out of the bundle
78
+ // because the policy copies an allow-list of state entries, not because
79
+ // the roots are apart.
80
+ configRoot: input.workspaceRoot,
81
+ allowShared: true,
82
+ destinationRoot: stateRoot,
83
+ sessionId: input.sessionId,
84
+ ...(input.lease ? { lease: input.lease } : {}),
85
+ requireQuiescent: true,
86
+ includeSubtree: true,
87
+ omitDiagnostics: true,
88
+ });
89
+ }
90
+ catch (error) {
91
+ const failure = asExportFailure(error, input.workspaceRoot);
92
+ if (failure)
93
+ return { ok: false, reason: failure };
94
+ throw error;
95
+ }
96
+ const manifest = {
97
+ format: 'maka.session-export',
98
+ formatVersion: 1,
99
+ exportedAt: (input.now ?? Date.now)(),
100
+ rootSessionId: plan.sessionId,
101
+ sessionIds: plan.sessionIds,
102
+ schema: plan.sourceSchema,
103
+ connection: plan.connection,
104
+ includedEntries: plan.includedEntries,
105
+ excludedEntries: plan.excludedEntries,
106
+ diagnosticsOmitted: true,
107
+ omittedEventTypes: SESSION_BUNDLE_OMITTED_EVENT_TYPES,
108
+ };
109
+ const stateIdentity = {
110
+ mediaType: SESSION_EXPORT_MEDIA_TYPE,
111
+ bytes: Buffer.from(`${JSON.stringify(manifest)}\n`, 'utf8'),
112
+ };
113
+ const artifact = await createSessionBundleFileService().pack({
114
+ snapshot: { stateRoot, workspaceRoot, stateIdentity },
115
+ envelope: { sessionId: plan.sessionId },
116
+ destination: input.destination,
117
+ limits: input.limits ?? SESSION_EXPORT_BUNDLE_LIMITS,
118
+ });
119
+ return { ok: true, artifact, export: manifest };
120
+ }
121
+ catch (error) {
122
+ if (error instanceof SessionBundleFileError) {
123
+ if (error.details?.quota !== undefined) {
124
+ return { ok: false, reason: { kind: 'bundle_limit', details: error.details } };
125
+ }
126
+ // The precheck fails fast, but the packer publishes atomically and can
127
+ // still lose the race. Reporting that as a generic IO failure would give
128
+ // a caller a different exit code for the same outcome.
129
+ if (error.code === 'destination_exists') {
130
+ return { ok: false, reason: { kind: 'destination_exists' } };
131
+ }
132
+ return { ok: false, reason: { kind: 'io_failed', message: error.message } };
133
+ }
134
+ const failure = asExportFailure(error, input.workspaceRoot);
135
+ if (failure)
136
+ return { ok: false, reason: failure };
137
+ return { ok: false, reason: { kind: 'io_failed', message: String(error) } };
138
+ }
139
+ finally {
140
+ await rm(staging, { recursive: true, force: true }).catch(() => { });
141
+ }
142
+ }
143
+ function asExportFailure(error, workspaceRoot) {
144
+ if (!(error instanceof SessionBundleExportError))
145
+ return undefined;
146
+ switch (error.code) {
147
+ case 'session_active':
148
+ return { kind: 'session_active', message: error.message };
149
+ case 'schema_unsupported':
150
+ return { kind: 'schema_unsupported', message: error.message };
151
+ case 'symlink':
152
+ case 'path_escape':
153
+ case 'unsupported_entry':
154
+ return { kind: 'artifact_unsafe', message: error.message };
155
+ case 'missing_entry':
156
+ return { kind: 'artifact_missing', message: error.message };
157
+ case 'invalid_root':
158
+ // The policy reports a missing Session through this code too, and only
159
+ // the message separates it from a directory that is not a workspace.
160
+ return /session does not exist/i.test(error.message)
161
+ ? { kind: 'session_not_found' }
162
+ : { kind: 'workspace_not_found', workspaceRoot };
163
+ case 'destination_not_empty':
164
+ return { kind: 'destination_exists' };
165
+ default:
166
+ return { kind: 'io_failed', message: error.message };
167
+ }
168
+ }
169
+ async function isDirectory(path) {
170
+ return stat(path)
171
+ .then((metadata) => metadata.isDirectory())
172
+ .catch(() => false);
173
+ }
174
+ async function exists(path) {
175
+ return stat(path)
176
+ .then(() => true)
177
+ .catch(() => false);
178
+ }
@@ -0,0 +1,100 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ /**
20
+ * Import a `.maka-session` bundle into a workspace.
21
+ *
22
+ * The mirror of the export: `hydrate()` verifies and unpacks what `pack()`
23
+ * sealed, and `importSessionBundleState()` merges what the export policy
24
+ * prepared. This module is the thin part -- verify, merge, report.
25
+ *
26
+ * The bundle names its connection by slug; nothing here resolves it. A Session
27
+ * whose slug this workspace does not have arrives in the state the app already
28
+ * shows for that: stale, and routable once the user picks a connection.
29
+ */
30
+ import { mkdtemp, rm, stat } from 'node:fs/promises';
31
+ import { join } from 'node:path';
32
+ import { tmpdir } from 'node:os';
33
+ import { SessionBundleFileError, } from '@maka/storage/session-bundle-contract';
34
+ import { createSessionBundleFileService } from '@maka/storage/session-bundle-file-service';
35
+ import { importSessionBundleState, SessionBundleImportError, } from '@maka/storage/session-bundle-policy';
36
+ import { SESSION_EXPORT_BUNDLE_LIMITS } from './session-export.js';
37
+ export async function importSessionBundle(input) {
38
+ if (!(await isDirectory(input.workspaceRoot))) {
39
+ return {
40
+ ok: false,
41
+ reason: { kind: 'workspace_not_found', workspaceRoot: input.workspaceRoot },
42
+ };
43
+ }
44
+ const limits = input.limits ?? SESSION_EXPORT_BUNDLE_LIMITS;
45
+ const staging = await mkdtemp(join(tmpdir(), 'maka-session-import-'));
46
+ try {
47
+ const service = createSessionBundleFileService();
48
+ // Read the identity before hydrating: the codec verifies a bundle against a
49
+ // Session id, and the bundle is the only thing that knows which one.
50
+ const inspection = await service.inspect({ source: { path: input.source }, limits });
51
+ const manifest = readExportManifest(inspection.stateIdentity.bytes);
52
+ const hydration = await service.hydrate({
53
+ source: { path: input.source },
54
+ limits,
55
+ expectedSessionId: manifest?.rootSessionId ?? inspection.manifest.envelope.sessionId,
56
+ destinationRoot: join(staging, 'hydrated'),
57
+ });
58
+ const merged = await importSessionBundleState({
59
+ stateRoot: input.workspaceRoot,
60
+ bundleStateRoot: hydration.stateRoot,
61
+ ...(input.lease ? { lease: input.lease } : {}),
62
+ });
63
+ return { ok: true, ...merged, manifest };
64
+ }
65
+ catch (error) {
66
+ if (error instanceof SessionBundleImportError) {
67
+ switch (error.code) {
68
+ case 'session_exists':
69
+ return { ok: false, reason: { kind: 'session_exists', message: error.message } };
70
+ case 'schema_unsupported':
71
+ return { ok: false, reason: { kind: 'schema_unsupported', message: error.message } };
72
+ case 'conflict':
73
+ return { ok: false, reason: { kind: 'conflict', message: error.message } };
74
+ default:
75
+ return { ok: false, reason: { kind: 'io_failed', message: error.message } };
76
+ }
77
+ }
78
+ if (error instanceof SessionBundleFileError) {
79
+ return { ok: false, reason: { kind: 'bundle_unreadable', message: error.message } };
80
+ }
81
+ return { ok: false, reason: { kind: 'io_failed', message: String(error) } };
82
+ }
83
+ finally {
84
+ await rm(staging, { recursive: true, force: true }).catch(() => { });
85
+ }
86
+ }
87
+ function readExportManifest(bytes) {
88
+ try {
89
+ const parsed = JSON.parse(Buffer.from(bytes).toString('utf8'));
90
+ return parsed.format === 'maka.session-export' ? parsed : undefined;
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ }
96
+ async function isDirectory(path) {
97
+ return stat(path)
98
+ .then((metadata) => metadata.isDirectory())
99
+ .catch(() => false);
100
+ }
@@ -3608,7 +3608,12 @@ function buildAgentOutputCommittedResult(input) {
3608
3608
  truncated: withoutText.omittedArtifactIds > 0,
3609
3609
  };
3610
3610
  }
3611
- const codePoints = Array.from(text);
3611
+ const codePoints = [];
3612
+ for (const point of text) {
3613
+ if (codePoints.length >= input.maxBytes)
3614
+ break;
3615
+ codePoints.push(point);
3616
+ }
3612
3617
  let low = 0;
3613
3618
  let high = codePoints.length;
3614
3619
  let best = { ...withoutText, textTruncated: true };
@@ -1677,13 +1677,16 @@ function normalizeBackgroundTimeoutMs(value) {
1677
1677
  }
1678
1678
  return value;
1679
1679
  }
1680
- function splitPtyData(data) {
1681
- const codePoints = Array.from(data);
1682
- const chunks = [];
1683
- for (let offset = 0; offset < codePoints.length; offset += PTY_RAW_INPUT_CHUNK_CODE_POINTS) {
1684
- chunks.push(codePoints.slice(offset, offset + PTY_RAW_INPUT_CHUNK_CODE_POINTS).join(''));
1680
+ function* splitPtyData(data) {
1681
+ // Bound each published piece without materializing the entire callback as code points.
1682
+ let offset = 0;
1683
+ while (offset < data.length) {
1684
+ const start = offset;
1685
+ for (let count = 0; count < PTY_RAW_INPUT_CHUNK_CODE_POINTS && offset < data.length; count++) {
1686
+ offset += data.codePointAt(offset) > 0xffff ? 2 : 1;
1687
+ }
1688
+ yield data.slice(start, offset);
1685
1689
  }
1686
- return chunks;
1687
1690
  }
1688
1691
  function encodedPtyDataBytes(data) {
1689
1692
  return Buffer.byteLength(JSON.stringify(data), 'utf8');
@@ -212,16 +212,23 @@ function takeTailText(text, budget) {
212
212
  return { text: `${TRUNCATED_MARKER}\n${tail}`, truncated: true };
213
213
  }
214
214
  function sliceUtf8Tail(text, budget) {
215
- const characters = Array.from(text);
216
- let result = '';
215
+ const characters = [];
217
216
  let bytes = 0;
218
- for (let index = characters.length - 1; index >= 0; index -= 1) {
219
- const character = characters[index];
217
+ for (let end = text.length; end > 0;) {
218
+ let start = end - 1;
219
+ const last = text.charCodeAt(start);
220
+ if (last >= 0xdc00 && last <= 0xdfff && start > 0) {
221
+ const previous = text.charCodeAt(start - 1);
222
+ if (previous >= 0xd800 && previous <= 0xdbff)
223
+ start -= 1;
224
+ }
225
+ const character = text.slice(start, end);
220
226
  const size = Buffer.byteLength(character, 'utf8');
221
227
  if (bytes + size > budget)
222
228
  break;
223
- result = character + result;
229
+ characters.push(character);
224
230
  bytes += size;
231
+ end = start;
225
232
  }
226
- return result;
233
+ return characters.reverse().join('');
227
234
  }
@@ -286,17 +286,30 @@ export class AgentGraphCoordinator {
286
286
  /** Reconcile now and surface any host-level failure to explicit callers. */
287
287
  async reconcile(rootSessionId) {
288
288
  await this.#assertRootSupervisor(rootSessionId);
289
- const driver = await this.#driver(rootSessionId);
290
- driver.lastError = undefined;
291
- driver.paused = false;
292
- this.#requestDrive(driver);
293
- await this.waitForIdle(rootSessionId);
294
- if (driver.lastError !== undefined)
295
- throw driver.lastError;
296
- if (!driver.lastResult) {
297
- throw new Error(`Agent graph ${driver.graphId} produced no reconciliation result`);
289
+ let driver = await this.#driver(rootSessionId);
290
+ // A lookup started before handover can return after its driver retired.
291
+ while (driver.closed && !this.#closed)
292
+ driver = await this.#driver(rootSessionId);
293
+ driver.reconciliationReaders += 1;
294
+ try {
295
+ driver.lastError = undefined;
296
+ driver.paused = false;
297
+ this.#requestDrive(driver);
298
+ // An epoch handover must not redirect this caller to the next driver.
299
+ while (driver.task)
300
+ await driver.task;
301
+ if (driver.lastError !== undefined)
302
+ throw driver.lastError;
303
+ if (!driver.lastResult) {
304
+ throw new Error(`Agent graph ${driver.graphId} produced no reconciliation result`);
305
+ }
306
+ return driver.lastResult;
307
+ }
308
+ finally {
309
+ driver.reconciliationReaders -= 1;
310
+ if (driver.closed && driver.reconciliationReaders === 0)
311
+ driver.lastResult = undefined;
298
312
  }
299
- return driver.lastResult;
300
313
  }
301
314
  async waitForIdle(rootSessionId) {
302
315
  const driver = await this.#driver(rootSessionId);
@@ -1014,15 +1027,32 @@ export class AgentGraphCoordinator {
1014
1027
  const latest = await this.currentGraphEpoch(rootSessionId);
1015
1028
  if (latest.graphId !== current.graphId) {
1016
1029
  selected = latest;
1030
+ if (driver)
1031
+ void this.#retireDriver(driver);
1017
1032
  return;
1018
1033
  }
1019
1034
  if ((await this.#readSessionStateForGraph(rootSessionId, current.graphId)) !== 'terminal') {
1020
1035
  return;
1021
1036
  }
1022
1037
  selected = await this.advanceGraphEpoch(rootSessionId, current);
1038
+ if (driver)
1039
+ void this.#retireDriver(driver);
1023
1040
  });
1024
1041
  return selected;
1025
1042
  }
1043
+ async #retireDriver(driver) {
1044
+ // Tool closures can outlive their epoch. Fence them and let already
1045
+ // admitted operations finish before releasing their complete snapshots.
1046
+ // Cleanup belongs to the old epoch; its teardown I/O must not block the next.
1047
+ driver.closed = true;
1048
+ driver.requested = false;
1049
+ await Promise.allSettled([driver.task, driver.stopTask]);
1050
+ await this.#waitForClientProjectionUpdates(driver);
1051
+ if (driver.reconciliationReaders === 0)
1052
+ driver.lastResult = undefined;
1053
+ driver.runtimeFailureRunIds.clear();
1054
+ // Keep the lightweight driver for projection repair and close diagnostics.
1055
+ }
1026
1056
  async #readSessionStateForGraph(rootSessionId, graphId) {
1027
1057
  const snapshot = buildAgentGraphClientSnapshot(await this.#readClientModelInputForGraph(rootSessionId, graphId));
1028
1058
  if (snapshot.scheduleRevision === 0)
@@ -1094,6 +1124,7 @@ export class AgentGraphCoordinator {
1094
1124
  stopGeneration: 0,
1095
1125
  driveGeneration: 0,
1096
1126
  closed: false,
1127
+ reconciliationReaders: 0,
1097
1128
  clientProjectionDirty: false,
1098
1129
  runtimeFailureRunIds: new Set(),
1099
1130
  yieldWaiters: new Set(),
@@ -1125,6 +1156,8 @@ export class AgentGraphCoordinator {
1125
1156
  this.#requestDrive(driver);
1126
1157
  }
1127
1158
  #requestDrive(driver) {
1159
+ if (driver.closed || this.#closed)
1160
+ return;
1128
1161
  driver.requested = true;
1129
1162
  if (driver.task)
1130
1163
  return;
@@ -143,7 +143,12 @@ function truncateUtf8(text, maxBytes) {
143
143
  const ellipsisBytes = Buffer.byteLength(ellipsis, 'utf8');
144
144
  if (maxBytes < ellipsisBytes)
145
145
  return '';
146
- const codePoints = Array.from(text);
146
+ const codePoints = [];
147
+ for (const point of text) {
148
+ if (codePoints.length >= maxBytes)
149
+ break;
150
+ codePoints.push(point);
151
+ }
147
152
  let low = 0;
148
153
  let high = codePoints.length;
149
154
  let best = ellipsis;
@@ -151,10 +151,7 @@ async function checkedOpenAiCodexFetch(fetchFn, url, init, refreshOAuthAccessTok
151
151
  const response = await fetchFn(url, requestInit);
152
152
  if (response.ok)
153
153
  return response;
154
- const detail = await response
155
- .clone()
156
- .text()
157
- .catch(() => '');
154
+ const detail = await response.text().catch(() => '');
158
155
  if (response.status === 401 &&
159
156
  !authorizationReplayUsed &&
160
157
  refreshOAuthAccessToken &&
@@ -112,12 +112,12 @@ async function readWorkspaceInstructions(rootCandidate, scope) {
112
112
  if (seenDigests.has(digest))
113
113
  continue;
114
114
  seenDigests.add(digest);
115
- const chars = Array.from(cleaned).length;
115
+ const text = truncateCodepoints(cleaned, MAX_WORKSPACE_INSTRUCTION_FILE_CHARS);
116
116
  out.push({
117
117
  file,
118
118
  scope,
119
- text: truncateCodepoints(cleaned, MAX_WORKSPACE_INSTRUCTION_FILE_CHARS),
120
- truncated: chars > MAX_WORKSPACE_INSTRUCTION_FILE_CHARS,
119
+ text,
120
+ truncated: text.length < cleaned.length,
121
121
  });
122
122
  }
123
123
  catch {
@@ -130,8 +130,13 @@ function cleanPromptText(text) {
130
130
  return text.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '');
131
131
  }
132
132
  function truncateCodepoints(text, max) {
133
- const chars = Array.from(text);
134
- if (chars.length <= max)
133
+ if (text.length <= max)
135
134
  return text;
136
- return chars.slice(0, Math.max(0, max)).join('');
135
+ const chars = [];
136
+ for (const char of text) {
137
+ if (chars.length >= max)
138
+ return chars.join('');
139
+ chars.push(char);
140
+ }
141
+ return text;
137
142
  }
@@ -126,9 +126,9 @@ function mapTavilyRows(raw, limit) {
126
126
  continue;
127
127
  rows.push({
128
128
  provider: 'tavily',
129
- title: safeString(rawRow.title, location.url).slice(0, TAVILY_RESULT_TITLE_MAX_CHARS),
129
+ title: truncateResultText(safeString(rawRow.title, location.url), TAVILY_RESULT_TITLE_MAX_CHARS),
130
130
  url: location.url,
131
- snippet: safeString(rawRow.content).slice(0, TAVILY_RESULT_SNIPPET_MAX_CHARS),
131
+ snippet: truncateResultText(safeString(rawRow.content), TAVILY_RESULT_SNIPPET_MAX_CHARS),
132
132
  source: location.source,
133
133
  });
134
134
  if (rows.length >= limit)
@@ -166,6 +166,12 @@ async function readBoundedJson(response, maxBytes) {
166
166
  function safeString(value, fallback = '') {
167
167
  return typeof value === 'string' ? value : fallback;
168
168
  }
169
+ function truncateResultText(value, maxChars) {
170
+ if (value.length <= maxChars)
171
+ return value;
172
+ // Detach bounded results from large provider strings, preserving even unpaired UTF-16 surrogates.
173
+ return Buffer.from(value.slice(0, maxChars), 'utf16le').toString('utf16le');
174
+ }
169
175
  function normalizeResultLocation(value) {
170
176
  if (typeof value !== 'string' || value.length > TAVILY_RESULT_URL_MAX_CHARS)
171
177
  return null;
@@ -0,0 +1,49 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ // Match split('\n').slice(offset, end).join('\n'), without creating entries
20
+ // for lines outside the requested window. Empty and trailing lines count too.
21
+ // A missing or zero limit is unbounded: end is the total line count.
22
+ export function readTextLineWindow(content, offset, limit) {
23
+ if (offset === undefined && limit === undefined)
24
+ return content;
25
+ let lineCount = 1;
26
+ for (let cursor = content.indexOf('\n'); cursor !== -1; cursor = content.indexOf('\n', cursor + 1)) {
27
+ lineCount++;
28
+ }
29
+ const start = offset ?? 0;
30
+ const end = limit ? start + limit : lineCount;
31
+ const from = sliceIndex(start, lineCount);
32
+ const to = sliceIndex(end, lineCount);
33
+ if (from >= to)
34
+ return '';
35
+ const selected = [];
36
+ let cursor = 0;
37
+ for (let line = 0; line < to; line++) {
38
+ const newline = content.indexOf('\n', cursor);
39
+ const lineEnd = newline === -1 ? content.length : newline;
40
+ if (line >= from)
41
+ selected.push(content.slice(cursor, lineEnd));
42
+ cursor = lineEnd + 1;
43
+ }
44
+ return selected.join('\n');
45
+ }
46
+ function sliceIndex(value, length) {
47
+ const integer = Math.trunc(value) || 0;
48
+ return integer < 0 ? Math.max(length + integer, 0) : Math.min(integer, length);
49
+ }