@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
|
@@ -16,6 +16,7 @@ import type { AskUserFn } from "../tool-system/builtin/ask-user.js";
|
|
|
16
16
|
export interface PendingApproval {
|
|
17
17
|
resolve: (result: ApprovalResult) => void;
|
|
18
18
|
request: ApprovalRequest;
|
|
19
|
+
timer: ReturnType<typeof setTimeout>;
|
|
19
20
|
}
|
|
20
21
|
export interface PendingInput {
|
|
21
22
|
resolve: (answer: string) => void;
|
|
@@ -32,6 +33,7 @@ export interface RunLifecycleHooks {
|
|
|
32
33
|
export declare class RunApprovalBackend implements ApprovalBackend {
|
|
33
34
|
private pendingApproval;
|
|
34
35
|
private hooks;
|
|
36
|
+
private approvalRequestSeq;
|
|
35
37
|
setHooks(hooks: RunLifecycleHooks): void;
|
|
36
38
|
/** Max time (ms) to wait for approval before auto-rejecting. Default: 24h */
|
|
37
39
|
private timeoutMs;
|
|
@@ -42,6 +44,7 @@ export declare class RunApprovalBackend implements ApprovalBackend {
|
|
|
42
44
|
*/
|
|
43
45
|
resolveApproval(result: ApprovalResult): boolean;
|
|
44
46
|
hasPendingApproval(): boolean;
|
|
47
|
+
private supersedePendingApproval;
|
|
45
48
|
}
|
|
46
49
|
/**
|
|
47
50
|
* Creates an AskUserFn that suspends the run when user input is needed.
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
export class RunApprovalBackend {
|
|
15
15
|
pendingApproval = null;
|
|
16
16
|
hooks = null;
|
|
17
|
+
approvalRequestSeq = 0;
|
|
17
18
|
setHooks(hooks) {
|
|
18
19
|
this.hooks = hooks;
|
|
19
20
|
}
|
|
@@ -23,7 +24,8 @@ export class RunApprovalBackend {
|
|
|
23
24
|
this.timeoutMs = ms;
|
|
24
25
|
}
|
|
25
26
|
async requestApproval(request) {
|
|
26
|
-
|
|
27
|
+
const hooks = this.hooks;
|
|
28
|
+
if (!hooks) {
|
|
27
29
|
// No hooks wired — fail CLOSED. This backend is the engine's approval
|
|
28
30
|
// path for an interactive run; if a host forgot to setHooks() there is
|
|
29
31
|
// no UI to ask, so auto-approving every tool (the old behavior) silently
|
|
@@ -34,11 +36,8 @@ export class RunApprovalBackend {
|
|
|
34
36
|
reason: "run approval backend has no lifecycle hooks wired (denied fail-closed)",
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
|
-
|
|
38
|
-
const { approvalId } = await this.hooks.onApprovalNeeded(request);
|
|
39
|
-
// Suspend execution until resolved (with timeout safety net)
|
|
39
|
+
const requestSeq = ++this.approvalRequestSeq;
|
|
40
40
|
return new Promise((resolve) => {
|
|
41
|
-
this.pendingApproval = { resolve, request };
|
|
42
41
|
const timer = setTimeout(() => {
|
|
43
42
|
if (this.pendingApproval?.resolve === resolve) {
|
|
44
43
|
this.pendingApproval = null;
|
|
@@ -48,6 +47,30 @@ export class RunApprovalBackend {
|
|
|
48
47
|
if (typeof timer === "object" && "unref" in timer) {
|
|
49
48
|
timer.unref();
|
|
50
49
|
}
|
|
50
|
+
this.supersedePendingApproval();
|
|
51
|
+
this.pendingApproval = { resolve, request, timer };
|
|
52
|
+
void Promise.resolve()
|
|
53
|
+
.then(() => hooks.onApprovalNeeded(request))
|
|
54
|
+
.then(() => {
|
|
55
|
+
if (requestSeq !== this.approvalRequestSeq &&
|
|
56
|
+
this.pendingApproval?.resolve === resolve) {
|
|
57
|
+
this.pendingApproval = null;
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
resolve({
|
|
60
|
+
approved: false,
|
|
61
|
+
reason: "superseded: a newer approval request replaced this one before it was answered",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}, (err) => {
|
|
65
|
+
if (this.pendingApproval?.resolve !== resolve)
|
|
66
|
+
return;
|
|
67
|
+
this.pendingApproval = null;
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
resolve({
|
|
70
|
+
approved: false,
|
|
71
|
+
reason: `approval request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
51
74
|
});
|
|
52
75
|
}
|
|
53
76
|
/**
|
|
@@ -56,14 +79,26 @@ export class RunApprovalBackend {
|
|
|
56
79
|
resolveApproval(result) {
|
|
57
80
|
if (!this.pendingApproval)
|
|
58
81
|
return false;
|
|
59
|
-
const { resolve } = this.pendingApproval;
|
|
82
|
+
const { resolve, timer } = this.pendingApproval;
|
|
60
83
|
this.pendingApproval = null;
|
|
84
|
+
clearTimeout(timer);
|
|
61
85
|
resolve(result);
|
|
62
86
|
return true;
|
|
63
87
|
}
|
|
64
88
|
hasPendingApproval() {
|
|
65
89
|
return this.pendingApproval !== null;
|
|
66
90
|
}
|
|
91
|
+
supersedePendingApproval() {
|
|
92
|
+
if (!this.pendingApproval)
|
|
93
|
+
return;
|
|
94
|
+
const { resolve, timer } = this.pendingApproval;
|
|
95
|
+
this.pendingApproval = null;
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
resolve({
|
|
98
|
+
approved: false,
|
|
99
|
+
reason: "superseded: a newer approval request replaced this one before it was answered",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
67
102
|
}
|
|
68
103
|
// ─── RunAskUserAdapter ───────────────────────────────────────────
|
|
69
104
|
/**
|
package/dist/run/RunLock.js
CHANGED
|
@@ -10,6 +10,7 @@ import { join } from "node:path";
|
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { lock, check, unlock } from "../utils/lockfile.js";
|
|
12
12
|
import { logger } from "../logging/logger.js";
|
|
13
|
+
import { assertSafeRunId } from "./ids.js";
|
|
13
14
|
export class RunLock {
|
|
14
15
|
runsDir;
|
|
15
16
|
staleMs;
|
|
@@ -135,6 +136,7 @@ export class RunLock {
|
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
lockTarget(runId) {
|
|
139
|
+
assertSafeRunId(runId);
|
|
138
140
|
return join(this.runsDir, runId, "run.json");
|
|
139
141
|
}
|
|
140
142
|
}
|
package/dist/run/RunManager.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export declare class RunManager {
|
|
|
66
66
|
resume(runId: string, input?: ResumeRunInput): Promise<void>;
|
|
67
67
|
private resumeInner;
|
|
68
68
|
cancel(runId: string, reason?: string): Promise<void>;
|
|
69
|
+
private cancelInner;
|
|
69
70
|
get(runId: string): Promise<RunSnapshot | null>;
|
|
70
71
|
list(query?: ListRunsQuery): Promise<RunSnapshot[]>;
|
|
71
72
|
getEvents(runId: string): Promise<RunEvent[]>;
|
|
@@ -90,5 +91,6 @@ export declare class RunManager {
|
|
|
90
91
|
private transition;
|
|
91
92
|
private emitRunEvent;
|
|
92
93
|
private notifySubscribers;
|
|
94
|
+
private getPendingApprovalForResume;
|
|
93
95
|
private getOrThrow;
|
|
94
96
|
}
|
package/dist/run/RunManager.js
CHANGED
|
@@ -19,7 +19,13 @@ import { ArtifactTracker } from "./ArtifactTracker.js";
|
|
|
19
19
|
import { RunLock } from "./RunLock.js";
|
|
20
20
|
import { Heartbeat } from "./Heartbeat.js";
|
|
21
21
|
import { NoopEvaluator } from "./Evaluator.js";
|
|
22
|
+
import { assertSafeRunFileId, assertSafeRunId } from "./ids.js";
|
|
22
23
|
import { VALID_TRANSITIONS } from "./types.js";
|
|
24
|
+
function hasOwnString(value, key) {
|
|
25
|
+
return (value !== undefined &&
|
|
26
|
+
Object.prototype.hasOwnProperty.call(value, key) &&
|
|
27
|
+
typeof value[key] === "string");
|
|
28
|
+
}
|
|
23
29
|
export class RunManager {
|
|
24
30
|
store;
|
|
25
31
|
queue;
|
|
@@ -69,6 +75,9 @@ export class RunManager {
|
|
|
69
75
|
async submit(input) {
|
|
70
76
|
const now = Date.now();
|
|
71
77
|
const runId = nanoid(16);
|
|
78
|
+
if (input.parentRunId !== undefined && input.parentRunId !== null) {
|
|
79
|
+
assertSafeRunId(input.parentRunId);
|
|
80
|
+
}
|
|
72
81
|
const snapshot = {
|
|
73
82
|
runId,
|
|
74
83
|
objective: input.objective,
|
|
@@ -110,6 +119,7 @@ export class RunManager {
|
|
|
110
119
|
}
|
|
111
120
|
// ─── Resume ────────────────────────────────────────────────────
|
|
112
121
|
async resume(runId, input) {
|
|
122
|
+
assertSafeRunId(runId);
|
|
113
123
|
// Serialize resume/cancel decisions per run. Without this, two concurrent
|
|
114
124
|
// resumes (or resume racing cancel) both pass the status check below —
|
|
115
125
|
// status stays `waiting_*` until a later `await transition()` — and both
|
|
@@ -139,18 +149,15 @@ export class RunManager {
|
|
|
139
149
|
if (handle) {
|
|
140
150
|
if (run.status === "waiting_approval" && input?.approvalDecision) {
|
|
141
151
|
const { approvalId, approved, reason } = input.approvalDecision;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
reason,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
152
|
+
const approval = await this.getPendingApprovalForResume(run, approvalId);
|
|
153
|
+
approval.status = approved ? "approved" : "rejected";
|
|
154
|
+
approval.resolvedAt = Date.now();
|
|
155
|
+
await this.store.saveApproval(approval);
|
|
156
|
+
await this.emitRunEvent(runId, "approval_resolved", {
|
|
157
|
+
approvalId,
|
|
158
|
+
approved,
|
|
159
|
+
reason,
|
|
160
|
+
});
|
|
154
161
|
// Transition back to running and resolve the suspended Engine
|
|
155
162
|
await this.transition(run, "queued");
|
|
156
163
|
await this.transition(run, "running");
|
|
@@ -158,7 +165,7 @@ export class RunManager {
|
|
|
158
165
|
handle.resolveApproval(approved, reason);
|
|
159
166
|
return;
|
|
160
167
|
}
|
|
161
|
-
if (run.status === "waiting_input" && input
|
|
168
|
+
if (run.status === "waiting_input" && hasOwnString(input, "userInput")) {
|
|
162
169
|
await this.transition(run, "queued");
|
|
163
170
|
await this.transition(run, "running");
|
|
164
171
|
await this.emitRunEvent(runId, "run_resumed", { userInput: input.userInput });
|
|
@@ -177,17 +184,15 @@ export class RunManager {
|
|
|
177
184
|
// Re-queue the run for a fresh execution attempt
|
|
178
185
|
if (input?.approvalDecision) {
|
|
179
186
|
const { approvalId, approved, reason } = input.approvalDecision;
|
|
180
|
-
const approval = await this.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
});
|
|
190
|
-
}
|
|
187
|
+
const approval = await this.getPendingApprovalForResume(run, approvalId);
|
|
188
|
+
approval.status = approved ? "approved" : "rejected";
|
|
189
|
+
approval.resolvedAt = Date.now();
|
|
190
|
+
await this.store.saveApproval(approval);
|
|
191
|
+
await this.emitRunEvent(runId, "approval_resolved", {
|
|
192
|
+
approvalId,
|
|
193
|
+
approved,
|
|
194
|
+
reason,
|
|
195
|
+
});
|
|
191
196
|
}
|
|
192
197
|
await this.transition(run, "queued");
|
|
193
198
|
await this.emitRunEvent(runId, "run_resumed", {
|
|
@@ -197,6 +202,19 @@ export class RunManager {
|
|
|
197
202
|
}
|
|
198
203
|
// ─── Cancel ────────────────────────────────────────────────────
|
|
199
204
|
async cancel(runId, reason) {
|
|
205
|
+
assertSafeRunId(runId);
|
|
206
|
+
if (this.resolvingRuns.has(runId)) {
|
|
207
|
+
throw new Error(`Cannot cancel run ${runId}: another resume/cancel is already in progress`);
|
|
208
|
+
}
|
|
209
|
+
this.resolvingRuns.add(runId);
|
|
210
|
+
try {
|
|
211
|
+
await this.cancelInner(runId, reason);
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
this.resolvingRuns.delete(runId);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async cancelInner(runId, reason) {
|
|
200
218
|
const run = await this.getOrThrow(runId);
|
|
201
219
|
if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
|
|
202
220
|
throw new Error(`Cannot cancel run ${runId}: already in terminal state ${run.status}`);
|
|
@@ -225,16 +243,19 @@ export class RunManager {
|
|
|
225
243
|
}
|
|
226
244
|
// ─── Query ─────────────────────────────────────────────────────
|
|
227
245
|
async get(runId) {
|
|
246
|
+
assertSafeRunId(runId);
|
|
228
247
|
return this.store.get(runId);
|
|
229
248
|
}
|
|
230
249
|
async list(query) {
|
|
231
250
|
return this.store.list(query);
|
|
232
251
|
}
|
|
233
252
|
async getEvents(runId) {
|
|
253
|
+
assertSafeRunId(runId);
|
|
234
254
|
return this.store.listEvents(runId);
|
|
235
255
|
}
|
|
236
256
|
// ─── Attach (live stream) ──────────────────────────────────────
|
|
237
257
|
attach(runId, cb) {
|
|
258
|
+
assertSafeRunId(runId);
|
|
238
259
|
if (!this.subscribers.has(runId)) {
|
|
239
260
|
this.subscribers.set(runId, new Set());
|
|
240
261
|
}
|
|
@@ -326,6 +347,7 @@ export class RunManager {
|
|
|
326
347
|
}
|
|
327
348
|
// ─── Internal: Execute a run ───────────────────────────────────
|
|
328
349
|
async executeRun(runId) {
|
|
350
|
+
assertSafeRunId(runId);
|
|
329
351
|
const run = await this.getOrThrow(runId);
|
|
330
352
|
// Acquire run lock — prevents concurrent execution by multiple workers
|
|
331
353
|
const lockResult = await this.lock.acquire(runId);
|
|
@@ -657,7 +679,25 @@ export class RunManager {
|
|
|
657
679
|
}
|
|
658
680
|
}
|
|
659
681
|
// ─── Helpers ───────────────────────────────────────────────────
|
|
682
|
+
async getPendingApprovalForResume(run, approvalId) {
|
|
683
|
+
assertSafeRunFileId(approvalId, "approval id");
|
|
684
|
+
if (run.latestApprovalId !== approvalId) {
|
|
685
|
+
throw new Error(`Cannot resume run ${run.runId}: approval ${approvalId} is not the latest pending approval`);
|
|
686
|
+
}
|
|
687
|
+
const approval = await this.store.getApproval(run.runId, approvalId);
|
|
688
|
+
if (!approval) {
|
|
689
|
+
throw new Error(`Cannot resume run ${run.runId}: approval ${approvalId} was not found`);
|
|
690
|
+
}
|
|
691
|
+
if (approval.runId !== run.runId || approval.approvalId !== approvalId) {
|
|
692
|
+
throw new Error(`Cannot resume run ${run.runId}: approval ${approvalId} does not match this run`);
|
|
693
|
+
}
|
|
694
|
+
if (approval.status !== "pending") {
|
|
695
|
+
throw new Error(`Cannot resume run ${run.runId}: approval ${approvalId} is ${approval.status}, not pending`);
|
|
696
|
+
}
|
|
697
|
+
return approval;
|
|
698
|
+
}
|
|
660
699
|
async getOrThrow(runId) {
|
|
700
|
+
assertSafeRunId(runId);
|
|
661
701
|
const run = await this.store.get(runId);
|
|
662
702
|
if (!run)
|
|
663
703
|
throw new Error(`Run not found: ${runId}`);
|
package/dist/run/ids.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function assertSafeRunSegment(value, label) {
|
|
2
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
3
|
+
throw new Error(`invalid ${label}: must be a non-empty string`);
|
|
4
|
+
}
|
|
5
|
+
if (value.includes("/") || value.includes("\\")) {
|
|
6
|
+
throw new Error(`invalid ${label}: contains path separator: ${value}`);
|
|
7
|
+
}
|
|
8
|
+
if (value === "." || value === ".." || value.includes("..")) {
|
|
9
|
+
throw new Error(`invalid ${label}: contains parent-dir token: ${value}`);
|
|
10
|
+
}
|
|
11
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(value)) {
|
|
12
|
+
throw new Error(`invalid ${label}: unexpected characters: ${value}`);
|
|
13
|
+
}
|
|
14
|
+
if (value.length > 128) {
|
|
15
|
+
throw new Error(`invalid ${label}: too long (max 128 chars)`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function assertSafeRunId(runId) {
|
|
19
|
+
assertSafeRunSegment(runId, "run id");
|
|
20
|
+
}
|
|
21
|
+
export function assertSafeRunFileId(value, label) {
|
|
22
|
+
assertSafeRunSegment(value, label);
|
|
23
|
+
}
|
|
@@ -103,6 +103,7 @@ export declare class BackgroundShellManager {
|
|
|
103
103
|
/** Raw retained output (no cleaning) — test/UI helper. */
|
|
104
104
|
readOutputRaw(shellId: string): string | undefined;
|
|
105
105
|
listForSession(sessionId: string): BgShell[];
|
|
106
|
+
list(): BgShell[];
|
|
106
107
|
/**
|
|
107
108
|
* Read a shell's output. `mode=incremental` (default) returns text since the
|
|
108
109
|
* agent's last read; `mode=all` returns the full retained buffer. Output is
|
|
@@ -23,16 +23,16 @@
|
|
|
23
23
|
* worker discover and reap them (§难点1, `reapOrphansFromPidfiles`).
|
|
24
24
|
*/
|
|
25
25
|
import { spawn } from "node:child_process";
|
|
26
|
-
import { homedir } from "node:os";
|
|
27
26
|
import { join } from "node:path";
|
|
28
|
-
import { mkdirSync, writeFileSync, rmSync, existsSync, readdirSync, readFileSync
|
|
27
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync, readdirSync, readFileSync } from "node:fs";
|
|
29
28
|
import { StringDecoder } from "node:string_decoder";
|
|
30
29
|
import { utf8SafeCutLength } from "./utf8-cut.js";
|
|
31
|
-
import { resolveSpawnTarget, buildSandboxEnv, mergeShellEnv, killProcessGroup, groupAlive, defaultShellBinary } from "./spawn-common.js";
|
|
30
|
+
import { resolveSpawnTarget, buildSandboxEnv, mergeShellEnv, killProcessGroup, groupAlive, defaultShellBinary, } from "./spawn-common.js";
|
|
32
31
|
import { RingFile } from "./ring-file.js";
|
|
33
32
|
import { cleanOutput } from "./output-clean.js";
|
|
34
33
|
import { notificationQueue } from "../tool-system/builtin/agent-notifications.js";
|
|
35
34
|
import { logger } from "../logging/logger.js";
|
|
35
|
+
import { codeShellHome } from "../session/session-manager.js";
|
|
36
36
|
/** Per-session soft cap on background shells (design §7 — fork-bomb guard). */
|
|
37
37
|
export const MAX_SHELLS_PER_SESSION = 16;
|
|
38
38
|
/** Disk ring-file cap: keep the most recent 8MB of raw output (design §6). */
|
|
@@ -46,11 +46,10 @@ function nextShellId() {
|
|
|
46
46
|
// Short, collision-resistant enough within a process: counter + base36 of
|
|
47
47
|
// a monotonic-ish value. Avoids Date.now()/Math.random() (banned in some
|
|
48
48
|
// contexts and unnecessary here).
|
|
49
|
-
return `bg_${shellCounter.toString(36)}${(shellCounter * 2654435761 % 0xffffff).toString(36)}`;
|
|
49
|
+
return `bg_${shellCounter.toString(36)}${((shellCounter * 2654435761) % 0xffffff).toString(36)}`;
|
|
50
50
|
}
|
|
51
51
|
function bgShellsRoot() {
|
|
52
|
-
|
|
53
|
-
return join(base, ".code-shell", "bg-shells");
|
|
52
|
+
return join(codeShellHome(), "bg-shells");
|
|
54
53
|
}
|
|
55
54
|
const PORT_REGEXES = [
|
|
56
55
|
/localhost:(\d{2,5})/i,
|
|
@@ -78,7 +77,11 @@ export class BackgroundShellManager {
|
|
|
78
77
|
// the bg-shells root — refuse rather than trust the caller. Mirrors
|
|
79
78
|
// SessionManager.assertSafeSessionId without importing across the layer.
|
|
80
79
|
const sid = opts.sessionId;
|
|
81
|
-
if (typeof sid !== "string" ||
|
|
80
|
+
if (typeof sid !== "string" ||
|
|
81
|
+
sid.length === 0 ||
|
|
82
|
+
sid.includes("..") ||
|
|
83
|
+
sid.includes("/") ||
|
|
84
|
+
sid.includes("\\")) {
|
|
82
85
|
return { ok: false, error: `invalid sessionId for background shell` };
|
|
83
86
|
}
|
|
84
87
|
// Per-session soft cap (fork-bomb guard, §7).
|
|
@@ -220,9 +223,7 @@ export class BackgroundShellManager {
|
|
|
220
223
|
}
|
|
221
224
|
enqueueExitNotification(sh) {
|
|
222
225
|
const ok = sh.exitCode === 0;
|
|
223
|
-
const exitDesc = sh.signal
|
|
224
|
-
? `signal ${sh.signal}`
|
|
225
|
-
: `exit ${sh.exitCode ?? "?"}`;
|
|
226
|
+
const exitDesc = sh.signal ? `signal ${sh.signal}` : `exit ${sh.exitCode ?? "?"}`;
|
|
226
227
|
notificationQueue.enqueue({
|
|
227
228
|
agentId: sh.shellId,
|
|
228
229
|
// English description is the agent-facing wakeup text. The UI ignores
|
|
@@ -234,7 +235,9 @@ export class BackgroundShellManager {
|
|
|
234
235
|
status: ok ? "completed" : "failed",
|
|
235
236
|
workKind: "shell",
|
|
236
237
|
command: sh.command,
|
|
237
|
-
error: ok
|
|
238
|
+
error: ok
|
|
239
|
+
? undefined
|
|
240
|
+
: `Background shell ${sh.shellId} exited with ${exitDesc}. Use BashOutput("${sh.shellId}") to inspect.`,
|
|
238
241
|
enqueuedAt: Date.now(),
|
|
239
242
|
}, sh.sessionId);
|
|
240
243
|
}
|
|
@@ -267,6 +270,9 @@ export class BackgroundShellManager {
|
|
|
267
270
|
.filter((s) => s.sessionId === sessionId)
|
|
268
271
|
.map((s) => this.toPublic(s));
|
|
269
272
|
}
|
|
273
|
+
list() {
|
|
274
|
+
return [...this.shells.values()].map((s) => this.toPublic(s));
|
|
275
|
+
}
|
|
270
276
|
/**
|
|
271
277
|
* Read a shell's output. `mode=incremental` (default) returns text since the
|
|
272
278
|
* agent's last read; `mode=all` returns the full retained buffer. Output is
|
|
@@ -420,7 +426,9 @@ export class BackgroundShellManager {
|
|
|
420
426
|
try {
|
|
421
427
|
rmSync(path, { force: true });
|
|
422
428
|
}
|
|
423
|
-
catch {
|
|
429
|
+
catch {
|
|
430
|
+
/* ignore */
|
|
431
|
+
}
|
|
424
432
|
continue;
|
|
425
433
|
}
|
|
426
434
|
// Skip ones we already track in-process (this worker's own).
|
|
@@ -434,7 +442,9 @@ export class BackgroundShellManager {
|
|
|
434
442
|
try {
|
|
435
443
|
rmSync(path, { force: true });
|
|
436
444
|
}
|
|
437
|
-
catch {
|
|
445
|
+
catch {
|
|
446
|
+
/* ignore */
|
|
447
|
+
}
|
|
438
448
|
}
|
|
439
449
|
}
|
|
440
450
|
}
|
|
@@ -159,6 +159,13 @@ export function resolveGitBash() {
|
|
|
159
159
|
const override = process.env.CODE_SHELL_GIT_BASH_PATH;
|
|
160
160
|
if (override && existsSync(override))
|
|
161
161
|
return (gitBashCache = override);
|
|
162
|
+
// Test-only hermetic switch: skip auto-discovery entirely. Clearing PATH is
|
|
163
|
+
// NOT enough on Windows — CreateProcess always searches System32, so `where`
|
|
164
|
+
// still spawns and a CI runner's preinstalled Git for Windows would resolve a
|
|
165
|
+
// real bash.exe and defeat the fallback tests. This env flag makes the
|
|
166
|
+
// "Git Bash absent" scenario reproducible on a real Windows host.
|
|
167
|
+
if (process.env.CODE_SHELL_NO_SHELL_DISCOVERY)
|
|
168
|
+
return (gitBashCache = null) ?? undefined;
|
|
162
169
|
const candidates = [];
|
|
163
170
|
// (2) derive from `where git`. Git installs git.exe under either \cmd\ or
|
|
164
171
|
// \bin\; bash.exe lives under the sibling \bin\. Walk up to the Git root.
|
|
@@ -194,6 +201,9 @@ export function resolvePowerShell() {
|
|
|
194
201
|
const override = process.env.CODE_SHELL_POWERSHELL_PATH;
|
|
195
202
|
if (override && existsSync(override))
|
|
196
203
|
return (powerShellCache = override);
|
|
204
|
+
// Test-only hermetic switch (see resolveGitBash for why PATH-clearing fails).
|
|
205
|
+
if (process.env.CODE_SHELL_NO_SHELL_DISCOVERY)
|
|
206
|
+
return (powerShellCache = null) ?? undefined;
|
|
197
207
|
const candidates = [];
|
|
198
208
|
for (const exe of ["pwsh", "powershell"]) {
|
|
199
209
|
try {
|
|
@@ -27,10 +27,9 @@ export declare function recordDreamComplete(): void;
|
|
|
27
27
|
/**
|
|
28
28
|
* System prompt for the auto-dream tool-call loop.
|
|
29
29
|
*
|
|
30
|
-
* Drives the LLM as a "memory consolidation assistant" that operates
|
|
31
|
-
*
|
|
32
|
-
* read-only
|
|
33
|
-
* dream pass cannot obtain (no UI on the call path).
|
|
30
|
+
* Drives the LLM as a "memory consolidation assistant" that operates in dream
|
|
31
|
+
* scope and may maintain dream-owned user memories. Manual user memories are
|
|
32
|
+
* read-only and protected by the dispatcher.
|
|
34
33
|
*/
|
|
35
34
|
export declare function buildDreamSystemPrompt(): string;
|
|
36
35
|
/**
|
|
@@ -39,15 +38,27 @@ export declare function buildDreamSystemPrompt(): string;
|
|
|
39
38
|
* body. The LLM uses MemoryRead to fetch bodies for entries it wants to act on.
|
|
40
39
|
*/
|
|
41
40
|
export declare function buildDreamUserPrompt(userMemories: Array<{
|
|
41
|
+
id?: string;
|
|
42
42
|
name: string;
|
|
43
43
|
type: string;
|
|
44
44
|
description: string;
|
|
45
|
+
origin?: string;
|
|
46
|
+
useCount?: number;
|
|
47
|
+
updateCount?: number;
|
|
45
48
|
}>, dreamMemories: Array<{
|
|
49
|
+
id?: string;
|
|
46
50
|
name: string;
|
|
47
51
|
type: string;
|
|
48
52
|
description: string;
|
|
53
|
+
origin?: string;
|
|
54
|
+
useCount?: number;
|
|
55
|
+
updateCount?: number;
|
|
49
56
|
}>, globalMemories?: Array<{
|
|
57
|
+
id?: string;
|
|
50
58
|
name: string;
|
|
51
59
|
type: string;
|
|
52
60
|
description: string;
|
|
61
|
+
origin?: string;
|
|
62
|
+
useCount?: number;
|
|
63
|
+
updateCount?: number;
|
|
53
64
|
}>): string;
|
|
@@ -71,34 +71,34 @@ export function recordDreamComplete() {
|
|
|
71
71
|
/**
|
|
72
72
|
* System prompt for the auto-dream tool-call loop.
|
|
73
73
|
*
|
|
74
|
-
* Drives the LLM as a "memory consolidation assistant" that operates
|
|
75
|
-
*
|
|
76
|
-
* read-only
|
|
77
|
-
* dream pass cannot obtain (no UI on the call path).
|
|
74
|
+
* Drives the LLM as a "memory consolidation assistant" that operates in dream
|
|
75
|
+
* scope and may maintain dream-owned user memories. Manual user memories are
|
|
76
|
+
* read-only and protected by the dispatcher.
|
|
78
77
|
*/
|
|
79
78
|
export function buildDreamSystemPrompt() {
|
|
80
79
|
return [
|
|
81
80
|
"You are a memory consolidation assistant for the CodeShell memory system.",
|
|
82
81
|
"",
|
|
83
|
-
"Your job: clean up the `dream` scope by deduplicating, merging, removing stale, and improving descriptions.",
|
|
82
|
+
"Your job: clean up the `dream` scope by deduplicating, merging, removing stale, and improving descriptions, and promote only durable lessons into dream-owned `user` entries.",
|
|
84
83
|
"",
|
|
85
84
|
"Tools available (each takes `location`: 'global' = cross-project store, 'project' = this repo; default project):",
|
|
86
85
|
"- MemoryList({ scope, location }): list memories in a scope/location",
|
|
87
86
|
"- MemoryRead({ scope, location, name }): read full content of an entry",
|
|
88
|
-
"- MemorySave({ scope: 'dream', location, ... }): create or
|
|
89
|
-
"- MemoryDelete({ scope: 'dream', location, name }): soft-delete
|
|
87
|
+
"- MemorySave({ scope: 'dream'|'user', location, id?, ... }): create or update an entry by id",
|
|
88
|
+
"- MemoryDelete({ scope: 'dream'|'user', location, name }): soft-delete an owned entry (recoverable from trash)",
|
|
90
89
|
"",
|
|
91
|
-
"Consolidate BOTH the project dream
|
|
90
|
+
"Consolidate BOTH the project dream workspace AND the global dream workspace (pass location accordingly). Global dream is a cross-project workspace, not a dumping ground for single-project progress.",
|
|
92
91
|
"",
|
|
93
92
|
"Rules — read carefully:",
|
|
94
|
-
"1. You may
|
|
95
|
-
"2. You may
|
|
96
|
-
"3. If
|
|
97
|
-
"4.
|
|
98
|
-
"5.
|
|
99
|
-
"6.
|
|
100
|
-
|
|
101
|
-
"8.
|
|
93
|
+
"1. You may Save/Delete dream-scope entries only when they are origin:auto or origin:dream. Never modify origin:manual; missing origin means manual.",
|
|
94
|
+
"2. You may Save user-scope entries only for durable conclusions you own. The dispatcher will force origin:dream. You may update existing user entries only when they are origin:dream or origin:auto. Never modify/delete origin:manual user entries.",
|
|
95
|
+
"3. Use MemoryList first. If the same topic already has an id, update that id instead of creating a date, version, batch, or progress variant.",
|
|
96
|
+
"4. Cluster duplicates without vectors: compare stable topic words after removing dates, versions, batch numbers, and progress/completed wording.",
|
|
97
|
+
"5. Time-sensitivity rule: entries with dates, today/yesterday, progress snapshots, completed fixes, review batches, or one-off task state should stay in dream, be merged, or be deleted. Do not promote them to user.",
|
|
98
|
+
"6. Durable lessons may be promoted to user only when they are reusable: user preferences, long-term project constraints, architecture decisions, root-cause lessons, non-obvious test/build traps, or stable references.",
|
|
99
|
+
'7. Archive COMPLETED work: dream entries that only record a finished fix/task ("已修", "已完成", "done") and carry no reusable lesson should be deleted, or merged into one compact topical entry. Keep any durable lesson by folding it into a date-free topic entry first.',
|
|
100
|
+
"8. Prefer fewer, higher-quality merged entries over many similar fragments. Be conservative: if uncertain whether two entries are truly duplicates, leave them alone.",
|
|
101
|
+
"9. When you're done, stop calling tools and respond with a one-paragraph summary of what you changed.",
|
|
102
102
|
].join("\n");
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
@@ -107,18 +107,18 @@ export function buildDreamSystemPrompt() {
|
|
|
107
107
|
* body. The LLM uses MemoryRead to fetch bodies for entries it wants to act on.
|
|
108
108
|
*/
|
|
109
109
|
export function buildDreamUserPrompt(userMemories, dreamMemories, globalMemories = []) {
|
|
110
|
-
const fmt = (m) => ` - [${m.type}] ${m.name}: ${m.description}`;
|
|
110
|
+
const fmt = (m) => ` - [${m.type}] ${m.name} (id:${m.id ?? "(none)"}, origin:${m.origin ?? "manual"}, use:${m.useCount ?? 0}, updates:${m.updateCount ?? 0}): ${m.description}`;
|
|
111
111
|
const listOrNone = (arr, noneMsg) => arr.length === 0 ? noneMsg : arr.map(fmt).join("\n");
|
|
112
112
|
const sections = [];
|
|
113
|
-
sections.push(`Project user-scope memories (READ-ONLY
|
|
113
|
+
sections.push(`Project user-scope memories (manual is READ-ONLY; origin:dream/auto may be maintained by id, ${userMemories.length} entries):`);
|
|
114
114
|
sections.push(listOrNone(userMemories, " (none)"));
|
|
115
115
|
sections.push("");
|
|
116
116
|
sections.push(`Project dream-scope memories (YOUR WORKSPACE — location:'project', ${dreamMemories.length} entries):`);
|
|
117
117
|
sections.push(listOrNone(dreamMemories, " (none — you may consolidate from user-scope by re-saving curated entries into dream)"));
|
|
118
118
|
sections.push("");
|
|
119
|
-
sections.push(`Global memories (cross-project — clean these too via location:'global', ${globalMemories.length} entries):`);
|
|
119
|
+
sections.push(`Global dream workspace memories (cross-project dream — clean these too via location:'global', ${globalMemories.length} entries):`);
|
|
120
120
|
sections.push(listOrNone(globalMemories, " (none)"));
|
|
121
121
|
sections.push("");
|
|
122
|
-
sections.push("Begin consolidation. Use MemoryRead to inspect any entries whose names suggest duplication or staleness, then MemorySave/MemoryDelete (with the right location) to clean up.");
|
|
122
|
+
sections.push("Begin consolidation. Use MemoryRead to inspect any entries whose names suggest duplication or staleness, then MemorySave/MemoryDelete (with the right id/location/scope) to clean up.");
|
|
123
123
|
return sections.join("\n");
|
|
124
124
|
}
|
|
@@ -13,9 +13,8 @@
|
|
|
13
13
|
* The loop is intentionally small and offline:
|
|
14
14
|
* - No streaming, no UI events — it runs in the background.
|
|
15
15
|
* - No permission prompts — there is no interactive backend on this path, so
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* freely.
|
|
16
|
+
* Save/Delete goes through an origin guard before dispatching. Dream may
|
|
17
|
+
* maintain origin:dream/auto entries but never touches origin:manual.
|
|
19
18
|
* - Capped at MAX_TURNS LLM round-trips and MAX_WRITES total mutations to
|
|
20
19
|
* bound damage on misbehavior.
|
|
21
20
|
*/
|