@cjhyy/code-shell-core 0.6.0-rc.9 → 0.7.0
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/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/automation/scheduler.d.ts +13 -7
- package/dist/automation/scheduler.js +116 -37
- package/dist/capability-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/agent-adapter.d.ts +4 -0
- package/dist/cc-orchestrator/agent-adapter.js +11 -1
- package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
- package/dist/cc-orchestrator/codex-session-history.js +65 -5
- package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
- package/dist/cc-orchestrator/cwd-normalize.js +19 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-changes.js +22 -5
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +265 -50
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
- package/dist/cc-orchestrator/session-history.d.ts +35 -0
- package/dist/cc-orchestrator/session-history.js +98 -15
- package/dist/cli/agent-server-stdio.js +9 -2
- package/dist/context/compaction.d.ts +8 -1
- package/dist/context/compaction.js +49 -4
- package/dist/context/manager.d.ts +16 -2
- package/dist/context/manager.js +103 -19
- package/dist/credentials/access.d.ts +57 -0
- package/dist/credentials/access.js +185 -0
- package/dist/credentials/index.d.ts +3 -1
- package/dist/credentials/index.js +2 -0
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +30 -11
- package/dist/credentials/oauth.d.ts +20 -0
- package/dist/credentials/oauth.js +114 -0
- package/dist/credentials/store.d.ts +1 -0
- package/dist/credentials/store.js +3 -1
- package/dist/credentials/types.d.ts +49 -3
- package/dist/credentials/use-credential-tool.d.ts +9 -1
- package/dist/credentials/use-credential-tool.js +58 -45
- package/dist/engine/engine.d.ts +23 -3
- package/dist/engine/engine.js +380 -255
- package/dist/engine/goal.d.ts +17 -0
- package/dist/engine/goal.js +16 -6
- package/dist/engine/image-policy.d.ts +6 -0
- package/dist/engine/image-policy.js +17 -6
- package/dist/engine/input-attachments.d.ts +13 -0
- package/dist/engine/input-attachments.js +398 -0
- package/dist/engine/model-facade.d.ts +5 -2
- package/dist/engine/model-facade.js +4 -4
- package/dist/engine/parse-task.d.ts +10 -0
- package/dist/engine/parse-task.js +5 -0
- package/dist/engine/run-image-input.d.ts +22 -0
- package/dist/engine/run-image-input.js +195 -0
- package/dist/engine/steer-queue.d.ts +3 -1
- package/dist/engine/steer-queue.js +10 -2
- package/dist/engine/streaming-tool-queue.d.ts +11 -7
- package/dist/engine/streaming-tool-queue.js +11 -7
- package/dist/engine/turn-loop.d.ts +37 -2
- package/dist/engine/turn-loop.js +228 -43
- package/dist/engine/types.d.ts +8 -0
- package/dist/git/worktree/crud.d.ts +69 -0
- package/dist/git/worktree/crud.js +206 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +82 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +51 -0
- package/dist/git/worktree/index.d.ts +5 -0
- package/dist/git/worktree/index.js +5 -0
- package/dist/git/worktree/query.d.ts +42 -0
- package/dist/git/worktree/query.js +121 -0
- package/dist/git/worktree/slug.d.ts +11 -0
- package/dist/git/worktree/slug.js +58 -0
- package/dist/git/worktree.d.ts +1 -84
- package/dist/git/worktree.js +5 -230
- package/dist/hooks/goal-stop-hook.d.ts +33 -1
- package/dist/hooks/goal-stop-hook.js +202 -34
- package/dist/index.d.ts +31 -28
- package/dist/index.js +27 -25
- package/dist/logging/logger.js +6 -6
- package/dist/logging/sanitize-messages.d.ts +10 -2
- package/dist/logging/sanitize-messages.js +21 -6
- package/dist/plugins/installer/checkUpdate.d.ts +4 -1
- package/dist/plugins/installer/checkUpdate.js +4 -2
- package/dist/plugins/installer/install.js +2 -0
- package/dist/plugins/installer/installFromSource.js +9 -1
- package/dist/plugins/installer/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- package/dist/plugins/installer/sourcePath.d.ts +9 -0
- package/dist/plugins/installer/sourcePath.js +50 -0
- package/dist/plugins/installer/update.d.ts +4 -1
- package/dist/plugins/installer/update.js +5 -3
- package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
- package/dist/plugins/parseMarketplaceInput.js +4 -3
- package/dist/plugins/pluginInstaller.js +24 -22
- package/dist/preset/index.d.ts +1 -0
- package/dist/preset/index.js +30 -13
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.d.ts +2 -0
- package/dist/protocol/chat-session-manager.js +38 -2
- package/dist/protocol/chat-session.d.ts +3 -0
- package/dist/protocol/chat-session.js +1 -0
- package/dist/protocol/client.d.ts +9 -4
- package/dist/protocol/client.js +18 -1
- package/dist/protocol/server.d.ts +30 -0
- package/dist/protocol/server.js +247 -47
- package/dist/protocol/types.d.ts +49 -0
- package/dist/protocol/types.js +6 -0
- package/dist/run/FileRunStore.js +10 -1
- package/dist/run/Heartbeat.js +12 -0
- package/dist/run/RunApprovalBackend.d.ts +3 -0
- package/dist/run/RunApprovalBackend.js +41 -6
- package/dist/run/RunLock.js +2 -0
- package/dist/run/RunManager.d.ts +2 -0
- package/dist/run/RunManager.js +64 -24
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +23 -0
- package/dist/runtime/background-shell.d.ts +1 -0
- package/dist/runtime/background-shell.js +23 -13
- package/dist/runtime/spawn-common.js +10 -0
- package/dist/services/auto-dream.d.ts +15 -4
- package/dist/services/auto-dream.js +20 -20
- package/dist/services/dream-consolidation.d.ts +2 -3
- package/dist/services/dream-consolidation.js +65 -15
- package/dist/services/extract-memories.d.ts +14 -5
- package/dist/services/extract-memories.js +20 -3
- package/dist/services/global-dream-promotion.d.ts +23 -0
- package/dist/services/global-dream-promotion.js +112 -0
- package/dist/services/memory-orchestrator.js +347 -34
- package/dist/session/memory.d.ts +61 -18
- package/dist/session/memory.js +342 -79
- package/dist/session/session-manager.d.ts +35 -1
- package/dist/session/session-manager.js +224 -4
- package/dist/session/transcript.d.ts +1 -1
- package/dist/session/transcript.js +17 -5
- package/dist/settings/manager.d.ts +1 -0
- package/dist/settings/manager.js +87 -37
- package/dist/settings/schema-export.d.ts +2 -3
- package/dist/settings/schema-export.js +2 -3
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/skills/scanner.d.ts +3 -2
- package/dist/skills/scanner.js +12 -9
- package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
- package/dist/tool-system/builtin/agent-notifications.js +19 -7
- package/dist/tool-system/builtin/background-jobs.d.ts +35 -6
- package/dist/tool-system/builtin/background-jobs.js +116 -7
- package/dist/tool-system/builtin/background-work.d.ts +17 -18
- package/dist/tool-system/builtin/background-work.js +51 -5
- package/dist/tool-system/builtin/config.d.ts +2 -1
- package/dist/tool-system/builtin/config.js +16 -11
- package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
- package/dist/tool-system/builtin/cron-list.definition.js +6 -0
- package/dist/tool-system/builtin/cron.d.ts +1 -2
- package/dist/tool-system/builtin/cron.js +9 -7
- package/dist/tool-system/builtin/drive-claude-code.d.ts +23 -2
- package/dist/tool-system/builtin/drive-claude-code.js +589 -48
- package/dist/tool-system/builtin/edit.js +5 -2
- package/dist/tool-system/builtin/generate-video.d.ts +1 -0
- package/dist/tool-system/builtin/generate-video.js +13 -4
- package/dist/tool-system/builtin/index.d.ts +8 -3
- package/dist/tool-system/builtin/index.js +50 -22
- package/dist/tool-system/builtin/lsp.d.ts +2 -1
- package/dist/tool-system/builtin/lsp.js +6 -3
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/notebook-edit.js +5 -2
- package/dist/tool-system/builtin/powershell.d.ts +5 -2
- package/dist/tool-system/builtin/powershell.js +11 -7
- package/dist/tool-system/builtin/read.js +118 -6
- package/dist/tool-system/builtin/sleep.d.ts +1 -2
- package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
- package/dist/tool-system/builtin/sleep.definition.js +28 -0
- package/dist/tool-system/builtin/sleep.js +1 -22
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/view-image.js +109 -19
- package/dist/tool-system/builtin/worktree.d.ts +4 -4
- package/dist/tool-system/builtin/worktree.js +297 -74
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +41 -1
- package/dist/tool-system/executor.d.ts +1 -5
- package/dist/tool-system/executor.js +94 -115
- package/dist/tool-system/mcp-manager.d.ts +31 -6
- package/dist/tool-system/mcp-manager.js +193 -78
- package/dist/tool-system/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +41 -12
- package/dist/tool-system/permission.d.ts +28 -7
- package/dist/tool-system/permission.js +130 -49
- package/dist/tool-system/registry.js +11 -4
- package/dist/tool-system/tool-result-redaction.d.ts +7 -0
- package/dist/tool-system/tool-result-redaction.js +48 -0
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +78 -11
- package/dist/utils/toolDisplay.js +1 -1
- package/package.json +4 -3
package/dist/protocol/server.js
CHANGED
|
@@ -24,6 +24,7 @@ import { backgroundJobRegistry } from "../tool-system/builtin/background-jobs.js
|
|
|
24
24
|
import { listBackgroundWorkForUI } from "../tool-system/builtin/background-work.js";
|
|
25
25
|
import { logger } from "../logging/logger.js";
|
|
26
26
|
import { nanoid } from "nanoid";
|
|
27
|
+
import { SessionManager } from "../session/session-manager.js";
|
|
27
28
|
import { redactLlmConfig, maskSecretValue } from "./redact.js";
|
|
28
29
|
import { redactSecrets } from "../logging/sanitize-messages.js";
|
|
29
30
|
const COMPACT_STREAM_STRATEGIES = new Set([
|
|
@@ -48,6 +49,15 @@ export class AgentServer {
|
|
|
48
49
|
settingsReader;
|
|
49
50
|
/** Disk-only active-goal reader for agent/goalGet on a non-live session. */
|
|
50
51
|
readActiveGoalFromDisk;
|
|
52
|
+
workspaceBridgeEnabled;
|
|
53
|
+
/**
|
|
54
|
+
* Last per-session EngineConfigSlice supplied by agent/run. Kept even after
|
|
55
|
+
* idle eviction so background-completion wakeups can rebuild the ChatSession
|
|
56
|
+
* with the same cwd/permission/trust inputs before draining the queue.
|
|
57
|
+
*/
|
|
58
|
+
lastSliceBySession = new Map();
|
|
59
|
+
/** Lazy disk reader for cold wakeup rehydrate when this process lacks a slice. */
|
|
60
|
+
diskSessionReader = null;
|
|
51
61
|
/**
|
|
52
62
|
* Monotonic config-reload version, bumped per reloadSettings request so each
|
|
53
63
|
* Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
|
|
@@ -73,6 +83,14 @@ export class AgentServer {
|
|
|
73
83
|
approvalTimers = new Map();
|
|
74
84
|
/** Default approval timeout: 5 minutes */
|
|
75
85
|
static APPROVAL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
86
|
+
/**
|
|
87
|
+
* Wall-clock timeout for AskUserQuestion during a GOAL run (10 minutes).
|
|
88
|
+
* Plain interactive asks stay untimed; only a self-driving goal run arms this
|
|
89
|
+
* so a mid-goal question can't suspend the loop forever with nobody watching.
|
|
90
|
+
* Longer than the tool-approval timeout — a user who IS present deserves more
|
|
91
|
+
* time to compose a real answer before we assume-and-continue.
|
|
92
|
+
*/
|
|
93
|
+
static GOAL_ASKUSER_TIMEOUT_MS = 10 * 60 * 1000;
|
|
76
94
|
/**
|
|
77
95
|
* Unsubscribe handle for the process-local `agentNotificationBus`
|
|
78
96
|
* subscription set up in the constructor. Called from `close()` so a
|
|
@@ -85,6 +103,7 @@ export class AgentServer {
|
|
|
85
103
|
this.legacyEngine = options.engine ?? null;
|
|
86
104
|
this.settingsReader = options.settingsReader ?? null;
|
|
87
105
|
this.readActiveGoalFromDisk = options.readActiveGoalFromDisk ?? null;
|
|
106
|
+
this.workspaceBridgeEnabled = options.workspaceBridge === true;
|
|
88
107
|
if (!this.chatManager && !this.legacyEngine) {
|
|
89
108
|
throw new Error("AgentServer: either chatManager or engine must be supplied");
|
|
90
109
|
}
|
|
@@ -169,7 +188,7 @@ export class AgentServer {
|
|
|
169
188
|
maybeWakeIdleSession(sessionId) {
|
|
170
189
|
if (!this.chatManager)
|
|
171
190
|
return;
|
|
172
|
-
const session = this.chatManager.get(sessionId);
|
|
191
|
+
const session = this.chatManager.get(sessionId) ?? this.rehydrateSessionForWake(sessionId);
|
|
173
192
|
if (!session || session.isBusy())
|
|
174
193
|
return;
|
|
175
194
|
// Headless / automation runs are one-shot: the caller takes result.text and
|
|
@@ -229,6 +248,76 @@ export class AgentServer {
|
|
|
229
248
|
this.maybeWakeIdleSession(sessionId);
|
|
230
249
|
});
|
|
231
250
|
}
|
|
251
|
+
rehydrateSessionForWake(sessionId) {
|
|
252
|
+
if (!this.chatManager)
|
|
253
|
+
return null;
|
|
254
|
+
try {
|
|
255
|
+
const pending = notificationQueue.getSnapshot(sessionId);
|
|
256
|
+
if (pending.length === 0) {
|
|
257
|
+
logger.debug("bg_wakeup.rehydrate_skipped_no_pending", { sessionId });
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
const slice = this.sliceForWakeRehydrate(sessionId);
|
|
261
|
+
if (!slice) {
|
|
262
|
+
logger.warn("bg_wakeup.rehydrate_skipped_missing_disk_session", {
|
|
263
|
+
sessionId,
|
|
264
|
+
pendingCount: pending.length,
|
|
265
|
+
reason: "state_json_cwd_missing",
|
|
266
|
+
});
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
if (!this.chatManager.sessionExistsOnDisk(sessionId, slice)) {
|
|
270
|
+
logger.warn("bg_wakeup.rehydrate_skipped_missing_disk_session", {
|
|
271
|
+
sessionId,
|
|
272
|
+
pendingCount: pending.length,
|
|
273
|
+
});
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
const session = this.chatManager.getOrCreate(sessionId, slice);
|
|
277
|
+
this.wireInteractiveSession(session, sessionId);
|
|
278
|
+
logger.debug("bg_wakeup.rehydrated_session", {
|
|
279
|
+
sessionId,
|
|
280
|
+
pendingCount: pending.length,
|
|
281
|
+
source: this.lastSliceBySession.has(sessionId) ? "last_slice" : "state_json",
|
|
282
|
+
});
|
|
283
|
+
return session;
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
logger.warn("bg_wakeup.rehydrate_failed", {
|
|
287
|
+
sessionId,
|
|
288
|
+
error: err instanceof Error ? err.message : String(err),
|
|
289
|
+
});
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
sliceForWakeRehydrate(sessionId) {
|
|
294
|
+
const cached = this.lastSliceBySession.get(sessionId);
|
|
295
|
+
if (cached)
|
|
296
|
+
return { ...cached };
|
|
297
|
+
if (!this.diskSessionReader)
|
|
298
|
+
this.diskSessionReader = new SessionManager();
|
|
299
|
+
const cwd = this.diskSessionReader.readCwd(sessionId);
|
|
300
|
+
if (!cwd)
|
|
301
|
+
return null;
|
|
302
|
+
return {
|
|
303
|
+
permissionMode: "default",
|
|
304
|
+
projectTrusted: false,
|
|
305
|
+
cwd,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
rememberSessionSlice(sessionId, slice) {
|
|
309
|
+
this.lastSliceBySession.set(sessionId, { ...slice });
|
|
310
|
+
}
|
|
311
|
+
wireInteractiveSession(session, sid) {
|
|
312
|
+
if (session.engine.isHeadless())
|
|
313
|
+
return;
|
|
314
|
+
session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
|
|
315
|
+
session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
|
|
316
|
+
session.engine.setInjectCredential((credentialId, credentialScope) => this.requestCredentialInjectForSession(session, sid, credentialId, credentialScope));
|
|
317
|
+
if (this.workspaceBridgeEnabled && typeof session.engine.setWorkspaceBridge === "function") {
|
|
318
|
+
session.engine.setWorkspaceBridge(this.makeWorkspaceBridge(session, sid));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
232
321
|
// ─── Request Dispatch ───────────────────────────────────────────
|
|
233
322
|
async handleRequest(req) {
|
|
234
323
|
switch (req.method) {
|
|
@@ -259,6 +348,9 @@ export class AgentServer {
|
|
|
259
348
|
case Methods.CloseSession:
|
|
260
349
|
this.handleCloseSession(req);
|
|
261
350
|
break;
|
|
351
|
+
case Methods.ReleaseWorkspace:
|
|
352
|
+
this.handleReleaseWorkspace(req);
|
|
353
|
+
break;
|
|
262
354
|
case Methods.GoalExtend:
|
|
263
355
|
this.handleGoalExtend(req);
|
|
264
356
|
break;
|
|
@@ -298,28 +390,39 @@ export class AgentServer {
|
|
|
298
390
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "task is required"));
|
|
299
391
|
return;
|
|
300
392
|
}
|
|
393
|
+
const sessionConfig = {
|
|
394
|
+
permissionMode: params.permissionMode,
|
|
395
|
+
cwd: params.cwd,
|
|
396
|
+
projectTrusted: params.projectTrusted,
|
|
397
|
+
goal: typeof params.goal === "string" || (params.goal != null && typeof params.goal === "object")
|
|
398
|
+
? params.goal
|
|
399
|
+
: undefined,
|
|
400
|
+
};
|
|
401
|
+
if (params.requireExisting === true && !cm.get(params.sessionId)) {
|
|
402
|
+
let existsOnDisk = false;
|
|
403
|
+
try {
|
|
404
|
+
existsOnDisk = cm.sessionExistsOnDisk(params.sessionId, sessionConfig);
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
const code = err.code ?? ErrorCodes.InternalError;
|
|
408
|
+
this.transport.send(createErrorResponse(req.id, code, err.message));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (!existsOnDisk) {
|
|
412
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `session ${params.sessionId} does not exist`));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
301
416
|
let session;
|
|
302
417
|
try {
|
|
303
|
-
session = cm.getOrCreate(params.sessionId,
|
|
304
|
-
permissionMode: params.permissionMode,
|
|
305
|
-
cwd: params.cwd,
|
|
306
|
-
projectTrusted: params.projectTrusted,
|
|
307
|
-
});
|
|
418
|
+
session = cm.getOrCreate(params.sessionId, sessionConfig);
|
|
308
419
|
}
|
|
309
420
|
catch (err) {
|
|
310
421
|
const code = err.code ?? ErrorCodes.InternalError;
|
|
311
422
|
this.transport.send(createErrorResponse(req.id, code, err.message));
|
|
312
423
|
return;
|
|
313
424
|
}
|
|
314
|
-
|
|
315
|
-
// running the prompt against a freshly-created blank session. A cron
|
|
316
|
-
// "continue this conversation" job whose session the user deleted must fail
|
|
317
|
-
// loudly here (SessionNotFound) so the scheduler can auto-disable it,
|
|
318
|
-
// instead of silently executing with no transcript/goal/context.
|
|
319
|
-
if (params.requireExisting === true && !session.engine.sessionExistsOnDisk(params.sessionId)) {
|
|
320
|
-
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `session ${params.sessionId} does not exist`));
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
425
|
+
this.rememberSessionSlice(params.sessionId, sessionConfig);
|
|
323
426
|
if (params.model !== undefined) {
|
|
324
427
|
if (typeof params.model !== "string" || params.model.length === 0) {
|
|
325
428
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "model must be a non-empty string"));
|
|
@@ -337,29 +440,14 @@ export class AgentServer {
|
|
|
337
440
|
session.engine.setPlanMode(params.planMode);
|
|
338
441
|
}
|
|
339
442
|
const sid = params.sessionId;
|
|
340
|
-
// Wire AskUserQuestion for this interactive session. The
|
|
341
|
-
// builds a fresh per-session Engine via engineFactory,
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
// chat (and in a resumed automation session). Route it to the client with
|
|
345
|
-
// this session's id so the renderer attributes the question to the right
|
|
346
|
-
// tab, resolving via the session's own pendingApprovals. Skip genuinely
|
|
347
|
-
// headless engines (no human to answer).
|
|
348
|
-
if (!session.engine.isHeadless()) {
|
|
349
|
-
session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
|
|
350
|
-
// Browser automation bridge: each method routes a browser action to the
|
|
351
|
-
// client (Electron main drives the webview via CDP) over the SAME
|
|
352
|
-
// request/response channel as askUser (pendingApprovals + requestId).
|
|
353
|
-
// Browser actions still keep their own bounded timeout; AskUserQuestion
|
|
354
|
-
// waits for a real answer or Stop/cancel.
|
|
355
|
-
session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
|
|
356
|
-
// Cookie→browser injection (InjectCredential tool): same cross-process
|
|
357
|
-
// channel; main restores the cookie jar into the built-in browser.
|
|
358
|
-
session.engine.setInjectCredential((credentialId) => this.requestCredentialInjectForSession(session, sid, credentialId));
|
|
359
|
-
}
|
|
443
|
+
// Wire AskUserQuestion and host bridges for this interactive session. The
|
|
444
|
+
// chatManager path builds a fresh per-session Engine via engineFactory, so
|
|
445
|
+
// these host callbacks must be installed after every create/recreate.
|
|
446
|
+
this.wireInteractiveSession(session, sid);
|
|
360
447
|
try {
|
|
361
448
|
const result = await session.enqueueTurn(params.task, {
|
|
362
449
|
cwd: params.cwd,
|
|
450
|
+
attachments: Array.isArray(params.attachments) ? params.attachments : undefined,
|
|
363
451
|
goal: typeof params.goal === "string" ||
|
|
364
452
|
(params.goal != null && typeof params.goal === "object")
|
|
365
453
|
? params.goal
|
|
@@ -467,9 +555,7 @@ export class AgentServer {
|
|
|
467
555
|
// the user already knows they pressed stop; clients should
|
|
468
556
|
// just clear busy.
|
|
469
557
|
const errName = err.name;
|
|
470
|
-
const aborted = runController.signal.aborted ||
|
|
471
|
-
errName === "AbortError" ||
|
|
472
|
-
errName === "APIUserAbortError";
|
|
558
|
+
const aborted = runController.signal.aborted || errName === "AbortError" || errName === "APIUserAbortError";
|
|
473
559
|
if (aborted) {
|
|
474
560
|
const cancelledResult = {
|
|
475
561
|
text: "",
|
|
@@ -623,6 +709,12 @@ export class AgentServer {
|
|
|
623
709
|
: params.sessionId
|
|
624
710
|
? (this.legacyEngine.clearGoal(params.sessionId) ?? false)
|
|
625
711
|
: false;
|
|
712
|
+
if (cleared && typeof params.sessionId === "string" && params.sessionId.length > 0) {
|
|
713
|
+
this.notify(Methods.StreamEvent, {
|
|
714
|
+
sessionId: params.sessionId,
|
|
715
|
+
event: { type: "goal_cleared" },
|
|
716
|
+
});
|
|
717
|
+
}
|
|
626
718
|
this.transport.send(createResponse(req.id, { ok: true, cleared }));
|
|
627
719
|
}
|
|
628
720
|
/**
|
|
@@ -720,7 +812,8 @@ export class AgentServer {
|
|
|
720
812
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
721
813
|
return;
|
|
722
814
|
}
|
|
723
|
-
const
|
|
815
|
+
const scope = params.scope === "all" ? "all" : "session";
|
|
816
|
+
const items = listBackgroundWorkForUI(sessionId, { scope });
|
|
724
817
|
this.transport.send(createResponse(req.id, { items }));
|
|
725
818
|
}
|
|
726
819
|
// ─── CloseSession ───────────────────────────────────────────────
|
|
@@ -747,6 +840,28 @@ export class AgentServer {
|
|
|
747
840
|
backgroundJobRegistry.dropForSession(params.sessionId);
|
|
748
841
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
749
842
|
}
|
|
843
|
+
// ─── ReleaseWorkspace ──────────────────────────────────────────
|
|
844
|
+
handleReleaseWorkspace(req) {
|
|
845
|
+
const params = (req.params ?? {});
|
|
846
|
+
if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
|
|
847
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (this.chatManager) {
|
|
851
|
+
const session = this.chatManager.get(params.sessionId);
|
|
852
|
+
if (!session) {
|
|
853
|
+
this.transport.send(createResponse(req.id, { ok: true, workspace: null }));
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const engine = session.engine;
|
|
857
|
+
const workspace = engine.releaseSessionWorkspace?.(params.sessionId) ?? null;
|
|
858
|
+
this.transport.send(createResponse(req.id, { ok: true, workspace }));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const engine = this.legacyEngine;
|
|
862
|
+
const workspace = engine?.releaseSessionWorkspace?.(params.sessionId) ?? null;
|
|
863
|
+
this.transport.send(createResponse(req.id, { ok: true, workspace }));
|
|
864
|
+
}
|
|
750
865
|
// ─── Configure ──────────────────────────────────────────────────
|
|
751
866
|
handleConfigure(req) {
|
|
752
867
|
const params = (req.params ?? {});
|
|
@@ -1239,8 +1354,7 @@ export class AgentServer {
|
|
|
1239
1354
|
const key = params.key;
|
|
1240
1355
|
if (!key)
|
|
1241
1356
|
throw new Error("provider_refresh: key required");
|
|
1242
|
-
const providers = engine.readSetting("providers") ??
|
|
1243
|
-
[];
|
|
1357
|
+
const providers = engine.readSetting("providers") ?? [];
|
|
1244
1358
|
const p = providers.find((x) => x.key === key);
|
|
1245
1359
|
if (!p)
|
|
1246
1360
|
throw new Error(`provider not found: ${key}`);
|
|
@@ -1276,8 +1390,7 @@ export class AgentServer {
|
|
|
1276
1390
|
if (refs.length > 0) {
|
|
1277
1391
|
throw new Error(`provider ${key} referenced by models: ${refs.join(", ")}`);
|
|
1278
1392
|
}
|
|
1279
|
-
const providers = engine.readSetting("providers") ??
|
|
1280
|
-
[];
|
|
1393
|
+
const providers = engine.readSetting("providers") ?? [];
|
|
1281
1394
|
const next = providers.filter((p) => p.key !== key);
|
|
1282
1395
|
engine.updateConfig("providers", next);
|
|
1283
1396
|
this.transport.send(createResponse(req.id, { type: "provider_delete", data: { ok: true } }));
|
|
@@ -1380,7 +1493,7 @@ export class AgentServer {
|
|
|
1380
1493
|
return;
|
|
1381
1494
|
}
|
|
1382
1495
|
try {
|
|
1383
|
-
const result = engine.enqueueSteer(params.sessionId, params.text, params.id, params.clientMessageId);
|
|
1496
|
+
const result = engine.enqueueSteer(params.sessionId, params.text, params.id, params.clientMessageId, params.attachments);
|
|
1384
1497
|
this.transport.send(createResponse(req.id, { ok: true, ...result }));
|
|
1385
1498
|
}
|
|
1386
1499
|
catch (err) {
|
|
@@ -1461,7 +1574,9 @@ export class AgentServer {
|
|
|
1461
1574
|
this.clearApprovalTimer(requestId);
|
|
1462
1575
|
const result = decision;
|
|
1463
1576
|
if (result && typeof result === "object" && "approved" in result) {
|
|
1464
|
-
resolve(result.approved
|
|
1577
|
+
resolve(result.approved
|
|
1578
|
+
? (result.answer ?? "")
|
|
1579
|
+
: (result.reason ?? "(user declined to answer)"));
|
|
1465
1580
|
}
|
|
1466
1581
|
else {
|
|
1467
1582
|
// chatManager approve handler resolves with the raw decision value.
|
|
@@ -1487,6 +1602,37 @@ export class AgentServer {
|
|
|
1487
1602
|
riskLevel: "low",
|
|
1488
1603
|
},
|
|
1489
1604
|
});
|
|
1605
|
+
// Goal mode is an unattended, self-driving run: nobody may be watching to
|
|
1606
|
+
// answer, and this ask channel otherwise has NO wall-clock timeout — so a
|
|
1607
|
+
// model that calls AskUserQuestion mid-goal would suspend the turn loop
|
|
1608
|
+
// forever (only a manual Stop could unblock it). When a goal is active,
|
|
1609
|
+
// arm the shared 10-minute approval timeout: on expiry, drain the pending
|
|
1610
|
+
// ask and resolve with a nudge so the loop keeps making progress instead
|
|
1611
|
+
// of hanging. Plain interactive (no-goal) asks keep their intentional
|
|
1612
|
+
// no-timeout behavior — a human can take as long as they like.
|
|
1613
|
+
let goalActive;
|
|
1614
|
+
try {
|
|
1615
|
+
goalActive = !!session.getGoal();
|
|
1616
|
+
}
|
|
1617
|
+
catch {
|
|
1618
|
+
goalActive = false;
|
|
1619
|
+
}
|
|
1620
|
+
if (goalActive) {
|
|
1621
|
+
const timer = setTimeout(() => {
|
|
1622
|
+
const resolveFn = session.pendingApprovals.get(requestId);
|
|
1623
|
+
if (resolveFn) {
|
|
1624
|
+
session.pendingApprovals.delete(requestId);
|
|
1625
|
+
this.approvalTimers.delete(requestId);
|
|
1626
|
+
resolveFn("(用户未在限定时间内回答;目标运行中请自行做出合理假设并继续推进。)");
|
|
1627
|
+
// Tell every client the ask is resolved (nobody answered) so the
|
|
1628
|
+
// stale AskUserQuestion card is dismissed instead of lingering as
|
|
1629
|
+
// if still waiting. Reuses the same envelope shape the desktop main
|
|
1630
|
+
// already broadcasts / the renderer's onApprovalResolved consumes.
|
|
1631
|
+
this.notify(Methods.ApprovalResolved, { sessionId, requestId });
|
|
1632
|
+
}
|
|
1633
|
+
}, AgentServer.GOAL_ASKUSER_TIMEOUT_MS);
|
|
1634
|
+
this.approvalTimers.set(requestId, timer);
|
|
1635
|
+
}
|
|
1490
1636
|
});
|
|
1491
1637
|
}
|
|
1492
1638
|
/**
|
|
@@ -1568,7 +1714,7 @@ export class AgentServer {
|
|
|
1568
1714
|
* calls restoreCookiesToBrowser and replies with a JSON result string we
|
|
1569
1715
|
* parse into { ok, count?, error? }. Degrades to ok:false on timeout/malformed.
|
|
1570
1716
|
*/
|
|
1571
|
-
requestCredentialInjectForSession(session, sessionId, credentialId) {
|
|
1717
|
+
requestCredentialInjectForSession(session, sessionId, credentialId, credentialScope = "full") {
|
|
1572
1718
|
return new Promise((resolve) => {
|
|
1573
1719
|
const requestId = nanoid(12);
|
|
1574
1720
|
session.pendingApprovals.set(requestId, (decision) => {
|
|
@@ -1605,13 +1751,67 @@ export class AgentServer {
|
|
|
1605
1751
|
requestId,
|
|
1606
1752
|
request: {
|
|
1607
1753
|
toolName: "__credential_action__",
|
|
1608
|
-
args: { action: "injectCookie", credentialId },
|
|
1754
|
+
args: { action: "injectCookie", credentialId, credentialScope },
|
|
1609
1755
|
description: `credential:inject:${credentialId}`,
|
|
1610
1756
|
riskLevel: "low",
|
|
1611
1757
|
},
|
|
1612
1758
|
});
|
|
1613
1759
|
});
|
|
1614
1760
|
}
|
|
1761
|
+
makeWorkspaceBridge(session, sessionId) {
|
|
1762
|
+
return {
|
|
1763
|
+
switch: (target) => this.requestWorkspaceSwitchForSession(session, sessionId, target),
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
requestWorkspaceSwitchForSession(session, sessionId, target) {
|
|
1767
|
+
return new Promise((resolve, reject) => {
|
|
1768
|
+
const requestId = nanoid(12);
|
|
1769
|
+
session.pendingApprovals.set(requestId, (decision) => {
|
|
1770
|
+
this.clearApprovalTimer(requestId);
|
|
1771
|
+
let raw;
|
|
1772
|
+
if (decision && typeof decision === "object" && "approved" in decision) {
|
|
1773
|
+
const r = decision;
|
|
1774
|
+
raw = r.approved ? r.answer : undefined;
|
|
1775
|
+
}
|
|
1776
|
+
else if (typeof decision === "string") {
|
|
1777
|
+
raw = decision;
|
|
1778
|
+
}
|
|
1779
|
+
if (raw === undefined) {
|
|
1780
|
+
reject(new Error("workspace switch declined or unavailable"));
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
try {
|
|
1784
|
+
const parsed = JSON.parse(raw);
|
|
1785
|
+
if ("ok" in parsed && parsed.ok === false) {
|
|
1786
|
+
reject(new Error(parsed.error ?? "workspace switch failed"));
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
resolve(parsed);
|
|
1790
|
+
}
|
|
1791
|
+
catch {
|
|
1792
|
+
reject(new Error("malformed workspace switch result"));
|
|
1793
|
+
}
|
|
1794
|
+
});
|
|
1795
|
+
const timer = setTimeout(() => {
|
|
1796
|
+
if (session.pendingApprovals.has(requestId)) {
|
|
1797
|
+
session.pendingApprovals.delete(requestId);
|
|
1798
|
+
this.approvalTimers.delete(requestId);
|
|
1799
|
+
reject(new Error("workspace switch timed out"));
|
|
1800
|
+
}
|
|
1801
|
+
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1802
|
+
this.approvalTimers.set(requestId, timer);
|
|
1803
|
+
this.notify(Methods.ApprovalRequest, {
|
|
1804
|
+
sessionId,
|
|
1805
|
+
requestId,
|
|
1806
|
+
request: {
|
|
1807
|
+
toolName: "__workspace_action__",
|
|
1808
|
+
args: { action: "switch", target },
|
|
1809
|
+
description: `workspace:switch:${target}`,
|
|
1810
|
+
riskLevel: "low",
|
|
1811
|
+
},
|
|
1812
|
+
});
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1615
1815
|
/**
|
|
1616
1816
|
* Ask the client to answer a question from the agent (legacy single-engine
|
|
1617
1817
|
* path). This intentionally has no wall-clock timeout; Stop/cancel drains
|
package/dist/protocol/types.d.ts
CHANGED
|
@@ -47,10 +47,41 @@ export declare const ErrorCodes: {
|
|
|
47
47
|
*/
|
|
48
48
|
readonly Cancelled: -32005;
|
|
49
49
|
};
|
|
50
|
+
export type InputAttachmentKind = "image" | "file" | "directory";
|
|
51
|
+
export type InputAttachmentOrigin = "paste" | "os-drop" | "file-panel" | "picker" | "mention" | "generated" | "tool";
|
|
52
|
+
export interface InputAttachmentMeta {
|
|
53
|
+
id: string;
|
|
54
|
+
sessionId: string;
|
|
55
|
+
kind: InputAttachmentKind;
|
|
56
|
+
origin: InputAttachmentOrigin;
|
|
57
|
+
path: string;
|
|
58
|
+
absPath: string;
|
|
59
|
+
relPath?: string;
|
|
60
|
+
mime?: string;
|
|
61
|
+
size: number;
|
|
62
|
+
sha256: string;
|
|
63
|
+
originalName?: string;
|
|
64
|
+
createdAt: number;
|
|
65
|
+
sourcePath?: string;
|
|
66
|
+
width?: number;
|
|
67
|
+
height?: number;
|
|
68
|
+
vision?: {
|
|
69
|
+
include: boolean;
|
|
70
|
+
mediaPath?: string;
|
|
71
|
+
detail?: "low" | "standard" | "high";
|
|
72
|
+
};
|
|
73
|
+
directory?: {
|
|
74
|
+
treePath?: string;
|
|
75
|
+
truncated?: boolean;
|
|
76
|
+
entryCount?: number;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
50
79
|
/** Start an agent run with a user task. */
|
|
51
80
|
export interface RunParams {
|
|
52
81
|
sessionId: string;
|
|
53
82
|
task: string;
|
|
83
|
+
/** Structured input attachments. Legacy `<codeshell-image>` task blocks remain supported. */
|
|
84
|
+
attachments?: InputAttachmentMeta[];
|
|
54
85
|
/** Stable id for the user's submit intent; duplicate ids are idempotent. */
|
|
55
86
|
clientMessageId?: string;
|
|
56
87
|
/**
|
|
@@ -123,6 +154,10 @@ export interface CancelParams {
|
|
|
123
154
|
export interface CloseSessionParams {
|
|
124
155
|
sessionId: string;
|
|
125
156
|
}
|
|
157
|
+
/** Reset a live session's workspace binding back to main. */
|
|
158
|
+
export interface ReleaseWorkspaceParams {
|
|
159
|
+
sessionId: string;
|
|
160
|
+
}
|
|
126
161
|
/** Inject context into a session transcript. */
|
|
127
162
|
export interface InjectParams {
|
|
128
163
|
sessionId: string;
|
|
@@ -132,6 +167,8 @@ export interface InjectParams {
|
|
|
132
167
|
export interface SteerParams {
|
|
133
168
|
sessionId: string;
|
|
134
169
|
text: string;
|
|
170
|
+
/** Structured input attachments that should ride with this queued steer. */
|
|
171
|
+
attachments?: InputAttachmentMeta[];
|
|
135
172
|
/** Stable host-side id for this queued draft. Rides through to the
|
|
136
173
|
* steer_injected event and is the handle Unsteer uses to revoke it. */
|
|
137
174
|
id?: string;
|
|
@@ -260,6 +297,12 @@ export interface ApprovalRequestNotification {
|
|
|
260
297
|
requestId: string;
|
|
261
298
|
request: ApprovalRequest;
|
|
262
299
|
}
|
|
300
|
+
/** Server tells clients a pending approval/ask has been resolved elsewhere. */
|
|
301
|
+
export interface ApprovalResolvedNotification {
|
|
302
|
+
/** Originating engine session when known. */
|
|
303
|
+
sessionId?: string;
|
|
304
|
+
requestId: string;
|
|
305
|
+
}
|
|
263
306
|
/** Server status changed. */
|
|
264
307
|
export interface StatusNotification {
|
|
265
308
|
status: "ready" | "running" | "error" | "shutdown";
|
|
@@ -279,6 +322,8 @@ export declare const Methods: {
|
|
|
279
322
|
readonly Unsteer: "agent/unsteer";
|
|
280
323
|
/** Close (destroy) a session. */
|
|
281
324
|
readonly CloseSession: "agent/closeSession";
|
|
325
|
+
/** Reset a session's workspace binding to main without closing it. */
|
|
326
|
+
readonly ReleaseWorkspace: "agent/releaseWorkspace";
|
|
282
327
|
/** Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). */
|
|
283
328
|
readonly GoalExtend: "agent/goalExtend";
|
|
284
329
|
/** Clear a session's persisted active goal (CC /goal clear). */
|
|
@@ -293,6 +338,10 @@ export declare const Methods: {
|
|
|
293
338
|
readonly BackgroundWork: "agent/backgroundWork";
|
|
294
339
|
readonly StreamEvent: "agent/streamEvent";
|
|
295
340
|
readonly ApprovalRequest: "agent/approvalRequest";
|
|
341
|
+
/** Server-initiated resolution of a pending approval/ask (e.g. a goal-mode
|
|
342
|
+
* AskUserQuestion that timed out) so every client can dismiss its stale card
|
|
343
|
+
* without a user decision. Same envelope the desktop main broadcasts. */
|
|
344
|
+
readonly ApprovalResolved: "agent/approvalResolved";
|
|
296
345
|
readonly Status: "agent/status";
|
|
297
346
|
};
|
|
298
347
|
export declare function createRequest(method: string, params?: Record<string, unknown>): RpcRequest;
|
package/dist/protocol/types.js
CHANGED
|
@@ -41,6 +41,8 @@ export const Methods = {
|
|
|
41
41
|
Unsteer: "agent/unsteer",
|
|
42
42
|
/** Close (destroy) a session. */
|
|
43
43
|
CloseSession: "agent/closeSession",
|
|
44
|
+
/** Reset a session's workspace binding to main without closing it. */
|
|
45
|
+
ReleaseWorkspace: "agent/releaseWorkspace",
|
|
44
46
|
/** Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). */
|
|
45
47
|
GoalExtend: "agent/goalExtend",
|
|
46
48
|
/** Clear a session's persisted active goal (CC /goal clear). */
|
|
@@ -56,6 +58,10 @@ export const Methods = {
|
|
|
56
58
|
// Server → Client (notifications, no id)
|
|
57
59
|
StreamEvent: "agent/streamEvent",
|
|
58
60
|
ApprovalRequest: "agent/approvalRequest",
|
|
61
|
+
/** Server-initiated resolution of a pending approval/ask (e.g. a goal-mode
|
|
62
|
+
* AskUserQuestion that timed out) so every client can dismiss its stale card
|
|
63
|
+
* without a user decision. Same envelope the desktop main broadcasts. */
|
|
64
|
+
ApprovalResolved: "agent/approvalResolved",
|
|
59
65
|
Status: "agent/status",
|
|
60
66
|
};
|
|
61
67
|
// ─── Helpers ────────────────────────────────────────────────────────
|
package/dist/run/FileRunStore.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import { mkdirSync, existsSync, readFileSync, writeFileSync, appendFileSync, readdirSync, renameSync, rmSync, } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { assertSafeRunFileId, assertSafeRunId } from "./ids.js";
|
|
15
17
|
export class FileRunStore {
|
|
16
18
|
runsDir;
|
|
17
19
|
constructor(storageDir) {
|
|
@@ -20,6 +22,7 @@ export class FileRunStore {
|
|
|
20
22
|
}
|
|
21
23
|
// ─── Helpers ───────────────────────────────────────────────────
|
|
22
24
|
runDir(runId) {
|
|
25
|
+
assertSafeRunId(runId);
|
|
23
26
|
return join(this.runsDir, runId);
|
|
24
27
|
}
|
|
25
28
|
ensureRunDir(runId) {
|
|
@@ -31,7 +34,7 @@ export class FileRunStore {
|
|
|
31
34
|
return dir;
|
|
32
35
|
}
|
|
33
36
|
writeJson(filePath, data) {
|
|
34
|
-
const tmp = filePath
|
|
37
|
+
const tmp = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
35
38
|
try {
|
|
36
39
|
writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
37
40
|
// Atomic rename — prevents partial writes on crash
|
|
@@ -153,6 +156,8 @@ export class FileRunStore {
|
|
|
153
156
|
}
|
|
154
157
|
// ─── Checkpoints ───────────────────────────────────────────────
|
|
155
158
|
async saveCheckpoint(cp) {
|
|
159
|
+
assertSafeRunId(cp.runId);
|
|
160
|
+
assertSafeRunFileId(cp.checkpointId, "checkpoint id");
|
|
156
161
|
const dir = this.runDir(cp.runId);
|
|
157
162
|
this.writeJson(join(dir, "checkpoints", `${cp.checkpointId}.json`), cp);
|
|
158
163
|
}
|
|
@@ -175,10 +180,14 @@ export class FileRunStore {
|
|
|
175
180
|
}
|
|
176
181
|
// ─── Approvals ─────────────────────────────────────────────────
|
|
177
182
|
async saveApproval(approval) {
|
|
183
|
+
assertSafeRunId(approval.runId);
|
|
184
|
+
assertSafeRunFileId(approval.approvalId, "approval id");
|
|
178
185
|
const dir = this.runDir(approval.runId);
|
|
179
186
|
this.writeJson(join(dir, "approvals", `${approval.approvalId}.json`), approval);
|
|
180
187
|
}
|
|
181
188
|
async getApproval(runId, approvalId) {
|
|
189
|
+
assertSafeRunId(runId);
|
|
190
|
+
assertSafeRunFileId(approvalId, "approval id");
|
|
182
191
|
return this.readJson(join(this.runDir(runId), "approvals", `${approvalId}.json`));
|
|
183
192
|
}
|
|
184
193
|
async getPendingApproval(runId) {
|
package/dist/run/Heartbeat.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
|
+
import { assertSafeRunId } from "./ids.js";
|
|
14
15
|
export class Heartbeat {
|
|
15
16
|
runsDir;
|
|
16
17
|
intervalMs;
|
|
@@ -23,6 +24,12 @@ export class Heartbeat {
|
|
|
23
24
|
* Start heartbeat for a run. Writes immediately, then repeats on interval.
|
|
24
25
|
*/
|
|
25
26
|
start(runId) {
|
|
27
|
+
assertSafeRunId(runId);
|
|
28
|
+
const existing = this.timers.get(runId);
|
|
29
|
+
if (existing) {
|
|
30
|
+
clearInterval(existing);
|
|
31
|
+
this.timers.delete(runId);
|
|
32
|
+
}
|
|
26
33
|
// Write first heartbeat immediately
|
|
27
34
|
this.write(runId);
|
|
28
35
|
// Schedule periodic heartbeats
|
|
@@ -37,6 +44,7 @@ export class Heartbeat {
|
|
|
37
44
|
* Stop heartbeat for a run and remove the heartbeat file.
|
|
38
45
|
*/
|
|
39
46
|
stop(runId) {
|
|
47
|
+
assertSafeRunId(runId);
|
|
40
48
|
const timer = this.timers.get(runId);
|
|
41
49
|
if (timer) {
|
|
42
50
|
clearInterval(timer);
|
|
@@ -56,6 +64,7 @@ export class Heartbeat {
|
|
|
56
64
|
* Read the last heartbeat for a run. Returns null if no heartbeat exists.
|
|
57
65
|
*/
|
|
58
66
|
read(runId) {
|
|
67
|
+
assertSafeRunId(runId);
|
|
59
68
|
const filePath = this.filePath(runId);
|
|
60
69
|
if (!existsSync(filePath))
|
|
61
70
|
return null;
|
|
@@ -71,6 +80,7 @@ export class Heartbeat {
|
|
|
71
80
|
* Returns true if stale or missing, false if recent.
|
|
72
81
|
*/
|
|
73
82
|
isStale(runId, thresholdMs) {
|
|
83
|
+
assertSafeRunId(runId);
|
|
74
84
|
const threshold = thresholdMs ?? this.intervalMs * 3;
|
|
75
85
|
const data = this.read(runId);
|
|
76
86
|
if (!data)
|
|
@@ -81,6 +91,7 @@ export class Heartbeat {
|
|
|
81
91
|
* Check if the process that wrote the heartbeat is still alive.
|
|
82
92
|
*/
|
|
83
93
|
isProcessAlive(runId) {
|
|
94
|
+
assertSafeRunId(runId);
|
|
84
95
|
const data = this.read(runId);
|
|
85
96
|
if (!data)
|
|
86
97
|
return false;
|
|
@@ -118,6 +129,7 @@ export class Heartbeat {
|
|
|
118
129
|
}
|
|
119
130
|
}
|
|
120
131
|
filePath(runId) {
|
|
132
|
+
assertSafeRunId(runId);
|
|
121
133
|
return join(this.runsDir, runId, "heartbeat");
|
|
122
134
|
}
|
|
123
135
|
}
|