@gajae-code/coding-agent 0.12.5 → 0.12.6
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/CHANGELOG.md +32 -0
- package/README.md +4 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +10 -8
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
- package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -3
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/sdk/host/host.d.ts +23 -2
- package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/artifacts.d.ts +8 -5
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/client-bridge.d.ts +14 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +5 -2
- package/dist/types/session/messages.d.ts +4 -1
- package/dist/types/session/session-manager.d.ts +6 -4
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/session/streaming-output.d.ts +32 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +178 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +3 -0
- package/dist/types/tools/output-meta.d.ts +37 -5
- package/dist/types/tools/tool-result.d.ts +1 -0
- package/dist/types/utils/artifact-id.d.ts +2 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/cursor.ts +20 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +425 -151
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
- package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/internal-urls/agent-protocol.ts +98 -38
- package/src/internal-urls/artifact-protocol.ts +28 -17
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/internal-urls/registry-helpers.ts +62 -0
- package/src/modes/acp/acp-agent.ts +51 -13
- package/src/modes/acp/acp-client-bridge.ts +154 -11
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/acp/adapter.ts +200 -9
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +691 -92
- package/src/sdk/bus/chat-daemon-control.ts +32 -6
- package/src/sdk/bus/index.ts +347 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -2
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/host/host.ts +187 -9
- package/src/sdk/host/reverse-leases.ts +249 -42
- package/src/sdk/session.ts +7 -6
- package/src/session/agent-session.ts +930 -354
- package/src/session/artifacts.ts +211 -74
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/client-bridge.ts +15 -0
- package/src/session/internal/managed-session-scope.ts +252 -67
- package/src/session/internal/managed-session-storage.ts +105 -5
- package/src/session/messages.ts +32 -7
- package/src/session/session-manager.ts +19 -21
- package/src/session/session-storage.ts +146 -24
- package/src/session/streaming-output.ts +198 -40
- package/src/task/index.ts +45 -0
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +4 -0
- package/src/task/render.ts +30 -0
- package/src/task/types.ts +34 -0
- package/src/tools/bash.ts +818 -239
- package/src/tools/eval.ts +99 -4
- package/src/tools/fetch.ts +77 -15
- package/src/tools/find.ts +7 -3
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +706 -116
- package/src/tools/search.ts +9 -4
- package/src/tools/tool-result.ts +1 -0
- package/src/utils/artifact-id.ts +6 -0
|
@@ -80,10 +80,32 @@ async function listFilesRecursively(rootPath: string): Promise<string[]> {
|
|
|
80
80
|
|
|
81
81
|
return files.sort((a, b) => a.localeCompare(b));
|
|
82
82
|
}
|
|
83
|
+
function localListingLabel(file: string): string {
|
|
84
|
+
return file.replace(/[\\[\]\r\n]/g, character => {
|
|
85
|
+
switch (character) {
|
|
86
|
+
case "\r":
|
|
87
|
+
return "\\r";
|
|
88
|
+
case "\n":
|
|
89
|
+
return "\\n";
|
|
90
|
+
default:
|
|
91
|
+
return `\\${character}`;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function localListingHref(file: string): string {
|
|
97
|
+
return file
|
|
98
|
+
.split("/")
|
|
99
|
+
.map(component => encodeURIComponent(component).replaceAll("(", "%28").replaceAll(")", "%29"))
|
|
100
|
+
.join("/");
|
|
101
|
+
}
|
|
83
102
|
|
|
84
103
|
async function buildListing(url: InternalUrl, localRoot: string): Promise<InternalResource> {
|
|
85
104
|
const files = await listFilesRecursively(localRoot);
|
|
86
|
-
const listing =
|
|
105
|
+
const listing =
|
|
106
|
+
files.length === 0
|
|
107
|
+
? "(empty)"
|
|
108
|
+
: files.map(file => `- [${localListingLabel(file)}](local://${localListingHref(file)})`).join("\n");
|
|
87
109
|
const content =
|
|
88
110
|
`# Local\n\n` +
|
|
89
111
|
`Session-scoped scratch space for large intermediate data, subagent handoffs, and reusable planning artifacts.\n\n` +
|
|
@@ -103,31 +125,30 @@ async function buildListing(url: InternalUrl, localRoot: string): Promise<Intern
|
|
|
103
125
|
function extractRelativePath(url: InternalUrl): string {
|
|
104
126
|
const host = url.rawHost || url.hostname;
|
|
105
127
|
const pathname = url.rawPathname ?? url.pathname;
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
? `${host}${pathname}`
|
|
110
|
-
: host
|
|
111
|
-
: pathname && pathname !== "/"
|
|
112
|
-
? pathname.slice(1)
|
|
113
|
-
: "";
|
|
114
|
-
|
|
115
|
-
if (!combined) {
|
|
116
|
-
return "";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let decoded: string;
|
|
128
|
+
const encodedAuthority = /^local:\/\/([^/?#]*)/i.exec(url.href)?.[1] ?? "";
|
|
129
|
+
const validatedHost = host;
|
|
130
|
+
let decodedPathname = pathname;
|
|
120
131
|
try {
|
|
121
|
-
|
|
132
|
+
const decodedAuthority = decodeURIComponent(encodedAuthority);
|
|
133
|
+
if (host && decodedAuthority !== host) throw new Error("authority_mismatch");
|
|
134
|
+
decodedPathname = decodeURIComponent(pathname.replaceAll("\\", "/"));
|
|
122
135
|
} catch {
|
|
123
136
|
throw new Error(`Invalid URL encoding in local:// path: ${url.href}`);
|
|
124
137
|
}
|
|
138
|
+
const combined = validatedHost
|
|
139
|
+
? decodedPathname && decodedPathname !== "/"
|
|
140
|
+
? `${validatedHost}${decodedPathname}`
|
|
141
|
+
: validatedHost
|
|
142
|
+
: decodedPathname && decodedPathname !== "/"
|
|
143
|
+
? decodedPathname.slice(1)
|
|
144
|
+
: "";
|
|
145
|
+
if (!combined) return "";
|
|
125
146
|
try {
|
|
126
|
-
validateRelativePath(
|
|
147
|
+
validateRelativePath(combined);
|
|
127
148
|
} catch (error) {
|
|
128
149
|
throw toLocalValidationError(error);
|
|
129
150
|
}
|
|
130
|
-
return
|
|
151
|
+
return combined;
|
|
131
152
|
}
|
|
132
153
|
|
|
133
154
|
function safeSessionId(options: LocalProtocolOptions): string {
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
* Shared helpers for internal-url protocol handlers that resolve session-scoped
|
|
3
3
|
* artifact IDs.
|
|
4
4
|
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "node:fs";
|
|
5
7
|
import * as path from "node:path";
|
|
8
|
+
import { openRecoveryFsRoot, type RecoveryFsRoot } from "@gajae-code/natives";
|
|
6
9
|
import type { ResolveContext } from "./types";
|
|
7
10
|
|
|
8
11
|
function addDir(dirs: string[], dir: string | null | undefined): void {
|
|
@@ -26,3 +29,62 @@ export function authorizedArtifactsDirsFromContext(context?: ResolveContext): st
|
|
|
26
29
|
for (const dir of context?.getAuthorizedArtifactsDirs?.() ?? []) addDir(dirs, dir);
|
|
27
30
|
return dirs;
|
|
28
31
|
}
|
|
32
|
+
|
|
33
|
+
export interface AuthorizedArtifactsRoot {
|
|
34
|
+
readonly directory: string;
|
|
35
|
+
readonly dev: bigint;
|
|
36
|
+
readonly ino: bigint;
|
|
37
|
+
readonly authority: RecoveryFsRoot;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AuthorizedArtifactSnapshot {
|
|
41
|
+
bytes: Buffer;
|
|
42
|
+
size: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function openAuthorizedArtifactsRoot(directory: string): AuthorizedArtifactsRoot {
|
|
46
|
+
if (process.platform !== "linux") throw new Error("artifact_authority_unavailable");
|
|
47
|
+
const named = fs.lstatSync(directory, { bigint: true });
|
|
48
|
+
if (!named.isDirectory() || named.isSymbolicLink()) throw new Error("unsafe_artifacts_root");
|
|
49
|
+
const authority = openRecoveryFsRoot(directory);
|
|
50
|
+
const retained = authority.identity();
|
|
51
|
+
if (
|
|
52
|
+
!retained.ok ||
|
|
53
|
+
!retained.identity ||
|
|
54
|
+
retained.identity.dev !== named.dev.toString() ||
|
|
55
|
+
retained.identity.ino !== named.ino.toString()
|
|
56
|
+
) {
|
|
57
|
+
authority.close();
|
|
58
|
+
throw new Error("unsafe_artifacts_root");
|
|
59
|
+
}
|
|
60
|
+
return { directory, dev: named.dev, ino: named.ino, authority };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function closeAuthorizedArtifactsRoot(root: AuthorizedArtifactsRoot): void {
|
|
64
|
+
root.authority.close();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function readAuthorizedArtifactFile(
|
|
68
|
+
root: AuthorizedArtifactsRoot,
|
|
69
|
+
filename: string,
|
|
70
|
+
maxBytes: number,
|
|
71
|
+
): AuthorizedArtifactSnapshot | null {
|
|
72
|
+
if (
|
|
73
|
+
path.basename(filename) !== filename ||
|
|
74
|
+
!Number.isSafeInteger(maxBytes) ||
|
|
75
|
+
maxBytes < 0 ||
|
|
76
|
+
maxBytes > 16 * 1024 * 1024
|
|
77
|
+
) {
|
|
78
|
+
throw new Error("unsafe_artifact_leaf");
|
|
79
|
+
}
|
|
80
|
+
if (!root.authority) throw new Error("artifact_authority_unavailable");
|
|
81
|
+
const captured = root.authority.read(filename, maxBytes);
|
|
82
|
+
if (!captured.ok) {
|
|
83
|
+
if (captured.code === "not_found") return null;
|
|
84
|
+
throw new Error(`unsafe_artifact_leaf:${captured.code ?? "capture_failed"}`);
|
|
85
|
+
}
|
|
86
|
+
if (!captured.data || !captured.identity) throw new Error("unsafe_artifact_leaf:missing_evidence");
|
|
87
|
+
const size = Number(captured.identity.size);
|
|
88
|
+
if (!Number.isSafeInteger(size) || size < 0) throw new Error("unsafe_artifact_leaf:invalid_size");
|
|
89
|
+
return { bytes: Buffer.from(captured.data), size };
|
|
90
|
+
}
|
|
@@ -660,6 +660,10 @@ export function createAcpReverseConnection(connection: AgentSideConnection, sess
|
|
|
660
660
|
"fs.readTextFile": "fs/read_text_file",
|
|
661
661
|
"fs.writeTextFile": "fs/write_text_file",
|
|
662
662
|
"terminal.create": "terminal/create",
|
|
663
|
+
"terminal.output": "terminal/output",
|
|
664
|
+
"terminal.waitForExit": "terminal/wait_for_exit",
|
|
665
|
+
"terminal.kill": "terminal/kill",
|
|
666
|
+
"terminal.release": "terminal/release",
|
|
663
667
|
"ui.elicit": "elicitation/create",
|
|
664
668
|
};
|
|
665
669
|
return {
|
|
@@ -668,6 +672,26 @@ export function createAcpReverseConnection(connection: AgentSideConnection, sess
|
|
|
668
672
|
params: JsonObject,
|
|
669
673
|
options?: { cancellationSignal?: AbortSignal },
|
|
670
674
|
): Promise<unknown> => {
|
|
675
|
+
if (method === "terminal.publish") {
|
|
676
|
+
const toolCallId = typeof params.toolCallId === "string" ? params.toolCallId : undefined;
|
|
677
|
+
const terminalId = typeof params.terminalId === "string" ? params.terminalId : undefined;
|
|
678
|
+
if (!toolCallId || !terminalId) {
|
|
679
|
+
throw new AcpSdkAdapterError(
|
|
680
|
+
"invalid_params",
|
|
681
|
+
"ACP terminal publication requires toolCallId and terminalId.",
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
await connection.sessionUpdate({
|
|
685
|
+
sessionId,
|
|
686
|
+
update: {
|
|
687
|
+
sessionUpdate: "tool_call_update",
|
|
688
|
+
toolCallId,
|
|
689
|
+
status: "in_progress",
|
|
690
|
+
content: [{ type: "terminal", terminalId }],
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
return {};
|
|
694
|
+
}
|
|
671
695
|
const name = methods[method];
|
|
672
696
|
if (!name)
|
|
673
697
|
throw new AcpSdkAdapterError("acp_reverse_unavailable", `ACP reverse method is unavailable: ${method}`);
|
|
@@ -718,6 +742,7 @@ export class AcpAgent implements Agent {
|
|
|
718
742
|
readonly #resolvingExisting = new Map<string, PendingAttachment>();
|
|
719
743
|
readonly #knownSessionCwds = new Map<string, string>();
|
|
720
744
|
readonly #knownSessionMetadata = new Map<string, { title?: string; updatedAt?: string }>();
|
|
745
|
+
readonly #pendingDeleteLocators = new Map<string, { cwd: string; path: string }>();
|
|
721
746
|
readonly #pendingCloseIdempotencyKeys = new Map<string, string>();
|
|
722
747
|
readonly #sessionEpochs = new Map<string, number>();
|
|
723
748
|
readonly #tearingDown = new Map<string, number>();
|
|
@@ -918,24 +943,35 @@ export class AcpAgent implements Agent {
|
|
|
918
943
|
|
|
919
944
|
async deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse> {
|
|
920
945
|
const record = this.#sessions.get(params.sessionId);
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
//
|
|
924
|
-
|
|
946
|
+
const pendingLocator = this.#pendingDeleteLocators.get(params.sessionId);
|
|
947
|
+
const cwd = record?.cwd ?? this.#knownSessionCwds.get(params.sessionId) ?? pendingLocator?.cwd;
|
|
948
|
+
// ACP's delete request has no cwd. Unknown ids remain the protocol no-op,
|
|
949
|
+
// while the broker can reconstruct an authenticated pending locator from its durable ledger.
|
|
950
|
+
if (!cwd) {
|
|
951
|
+
await (await this.#brokerAdapter()).global(
|
|
952
|
+
"session.delete",
|
|
953
|
+
{ sessionId: params.sessionId },
|
|
954
|
+
this.#lifecycleIdempotencyKey(params.sessionId, "session.delete"),
|
|
955
|
+
);
|
|
956
|
+
return {};
|
|
957
|
+
}
|
|
925
958
|
this.#beginTeardown(params.sessionId);
|
|
926
959
|
try {
|
|
927
960
|
await this.#teardownSession(params.sessionId, "deleted", true);
|
|
928
|
-
let saved:
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
961
|
+
let saved = pendingLocator?.cwd === cwd ? pendingLocator.path : undefined;
|
|
962
|
+
if (!saved) {
|
|
963
|
+
try {
|
|
964
|
+
saved = await this.#resolveSavedSession(params.sessionId, cwd);
|
|
965
|
+
} catch (error) {
|
|
966
|
+
if (error instanceof AcpSdkAdapterError && error.code === "not_found") {
|
|
967
|
+
this.#knownSessionCwds.delete(params.sessionId);
|
|
968
|
+
this.#knownSessionMetadata.delete(params.sessionId);
|
|
969
|
+
return {};
|
|
970
|
+
}
|
|
971
|
+
throw error;
|
|
936
972
|
}
|
|
937
|
-
throw error;
|
|
938
973
|
}
|
|
974
|
+
this.#pendingDeleteLocators.set(params.sessionId, { cwd, path: saved });
|
|
939
975
|
await (await this.#brokerAdapter()).global(
|
|
940
976
|
"session.delete",
|
|
941
977
|
{ sessionId: params.sessionId, sessionPath: saved, cwd, target: { path: cwd } },
|
|
@@ -943,6 +979,7 @@ export class AcpAgent implements Agent {
|
|
|
943
979
|
);
|
|
944
980
|
this.#knownSessionCwds.delete(params.sessionId);
|
|
945
981
|
this.#knownSessionMetadata.delete(params.sessionId);
|
|
982
|
+
this.#pendingDeleteLocators.delete(params.sessionId);
|
|
946
983
|
return {};
|
|
947
984
|
} finally {
|
|
948
985
|
this.#finishTeardown(params.sessionId);
|
|
@@ -2168,6 +2205,7 @@ export class AcpAgent implements Agent {
|
|
|
2168
2205
|
this.#resolvingExisting.clear();
|
|
2169
2206
|
this.#knownSessionCwds.clear();
|
|
2170
2207
|
this.#knownSessionMetadata.clear();
|
|
2208
|
+
this.#pendingDeleteLocators.clear();
|
|
2171
2209
|
this.#pendingCloseIdempotencyKeys.clear();
|
|
2172
2210
|
if (this.#closing.size === 0) this.#closing.clear();
|
|
2173
2211
|
this.#tearingDown.clear();
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* `AgentSession`) can route through the client when it advertises the
|
|
5
5
|
* relevant capabilities at `initialize` time.
|
|
6
6
|
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
7
9
|
import type {
|
|
8
10
|
PermissionOption as AcpPermissionOption,
|
|
9
11
|
TerminalHandle as AcpTerminalHandle,
|
|
@@ -19,10 +21,25 @@ import type {
|
|
|
19
21
|
ClientBridgePermissionOption,
|
|
20
22
|
ClientBridgePermissionOutcome,
|
|
21
23
|
ClientBridgePermissionToolCall,
|
|
24
|
+
ClientBridgeProviderLeaseIdentity,
|
|
22
25
|
ClientBridgeTerminalHandle,
|
|
23
26
|
} from "../../session/client-bridge";
|
|
24
27
|
import { resolveAcpPermissionMode } from "./permission-mode";
|
|
25
28
|
|
|
29
|
+
const ACP_TERMINAL_PUBLICATION_WAIT_MS = 500;
|
|
30
|
+
|
|
31
|
+
async function settleTerminalCleanup(operation: () => Promise<unknown>): Promise<boolean> {
|
|
32
|
+
return await Promise.race([
|
|
33
|
+
Promise.resolve()
|
|
34
|
+
.then(operation)
|
|
35
|
+
.then(
|
|
36
|
+
() => true,
|
|
37
|
+
() => false,
|
|
38
|
+
),
|
|
39
|
+
Bun.sleep(ACP_TERMINAL_PUBLICATION_WAIT_MS).then(() => false),
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
export { type AcpPermissionMode, resolveAcpPermissionMode } from "./permission-mode";
|
|
27
44
|
|
|
28
45
|
export function createAcpClientBridge(
|
|
@@ -39,6 +56,15 @@ export function createAcpClientBridge(
|
|
|
39
56
|
};
|
|
40
57
|
|
|
41
58
|
const bridge: ClientBridge = { capabilities, deferAgentInitiatedTurns: true };
|
|
59
|
+
const connectionId = `acp:${sessionId}`;
|
|
60
|
+
const connectionGeneration = 1;
|
|
61
|
+
const terminalProviderLeaseId = `${connectionId}:${randomUUID()}`;
|
|
62
|
+
const terminalProviderIdentity: ClientBridgeProviderLeaseIdentity = {
|
|
63
|
+
leaseId: terminalProviderLeaseId,
|
|
64
|
+
connectionId,
|
|
65
|
+
connectionGeneration,
|
|
66
|
+
fence: randomUUID(),
|
|
67
|
+
};
|
|
42
68
|
|
|
43
69
|
if (capabilities.readTextFile) {
|
|
44
70
|
bridge.readTextFile = async params => {
|
|
@@ -63,8 +89,49 @@ export function createAcpClientBridge(
|
|
|
63
89
|
}
|
|
64
90
|
|
|
65
91
|
if (capabilities.terminal) {
|
|
92
|
+
let terminalPublicationFailed = false;
|
|
93
|
+
bridge.quarantineProviderLease = async (providerLease, _reason) => {
|
|
94
|
+
if (
|
|
95
|
+
typeof providerLease === "string" ||
|
|
96
|
+
providerLease.leaseId !== terminalProviderIdentity.leaseId ||
|
|
97
|
+
providerLease.connectionId !== terminalProviderIdentity.connectionId ||
|
|
98
|
+
providerLease.connectionGeneration !== terminalProviderIdentity.connectionGeneration ||
|
|
99
|
+
providerLease.fence !== terminalProviderIdentity.fence
|
|
100
|
+
)
|
|
101
|
+
throw new Error("terminal provider lease mismatch");
|
|
102
|
+
terminalPublicationFailed = true;
|
|
103
|
+
};
|
|
104
|
+
let terminalCreationTail = Promise.resolve();
|
|
105
|
+
const withTerminalCreationAdmission = async <T>(operation: () => Promise<T>): Promise<T> => {
|
|
106
|
+
const previous = terminalCreationTail;
|
|
107
|
+
const release = Promise.withResolvers<void>();
|
|
108
|
+
terminalCreationTail = previous.then(() => release.promise);
|
|
109
|
+
await previous;
|
|
110
|
+
try {
|
|
111
|
+
return await operation();
|
|
112
|
+
} finally {
|
|
113
|
+
release.resolve();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
66
116
|
bridge.createTerminal = (params: ClientBridgeCreateTerminalParams) =>
|
|
67
|
-
|
|
117
|
+
withTerminalCreationAdmission(async () => {
|
|
118
|
+
if (terminalPublicationFailed) throw new Error("terminal provider quarantined after publication failure");
|
|
119
|
+
const creationTimeout = AbortSignal.timeout(ACP_TERMINAL_PUBLICATION_WAIT_MS);
|
|
120
|
+
const creationSignal = params.signal ? AbortSignal.any([params.signal, creationTimeout]) : creationTimeout;
|
|
121
|
+
return await createTerminalHandle(
|
|
122
|
+
connection,
|
|
123
|
+
sessionId,
|
|
124
|
+
params,
|
|
125
|
+
creationSignal,
|
|
126
|
+
terminalProviderIdentity,
|
|
127
|
+
() => {
|
|
128
|
+
terminalPublicationFailed = true;
|
|
129
|
+
},
|
|
130
|
+
() => {
|
|
131
|
+
terminalPublicationFailed = true;
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
});
|
|
68
135
|
}
|
|
69
136
|
|
|
70
137
|
if (promptPermission) {
|
|
@@ -79,21 +146,97 @@ async function createTerminalHandle(
|
|
|
79
146
|
connection: AgentSideConnection,
|
|
80
147
|
sessionId: string,
|
|
81
148
|
params: ClientBridgeCreateTerminalParams,
|
|
149
|
+
creationSignal: AbortSignal,
|
|
150
|
+
providerIdentity: ClientBridgeProviderLeaseIdentity,
|
|
151
|
+
onCreationUncertain: () => void,
|
|
152
|
+
onPublicationFailure: () => void,
|
|
82
153
|
): Promise<ClientBridgeTerminalHandle> {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
154
|
+
if (creationSignal.aborted) throw new Error("terminal creation aborted");
|
|
155
|
+
const creation = Promise.resolve().then(() =>
|
|
156
|
+
connection.createTerminal({
|
|
157
|
+
sessionId,
|
|
158
|
+
command: params.command,
|
|
159
|
+
...(params.args ? { args: params.args } : {}),
|
|
160
|
+
...(params.env ? { env: params.env } : {}),
|
|
161
|
+
...(params.cwd ? { cwd: params.cwd } : {}),
|
|
162
|
+
...(typeof params.outputByteLimit === "number" ? { outputByteLimit: params.outputByteLimit } : {}),
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
const creationAborted = Promise.withResolvers<"aborted">();
|
|
166
|
+
const onCreationAbort = () => creationAborted.resolve("aborted");
|
|
167
|
+
creationSignal.addEventListener("abort", onCreationAbort, { once: true });
|
|
168
|
+
if (creationSignal.aborted) creationAborted.resolve("aborted");
|
|
169
|
+
const creationOutcome = await Promise.race([
|
|
170
|
+
creation.then(
|
|
171
|
+
handle => ({ status: "created" as const, handle }),
|
|
172
|
+
error => ({ status: "failed" as const, error }),
|
|
173
|
+
),
|
|
174
|
+
creationAborted.promise.then(() => ({ status: "aborted" as const })),
|
|
175
|
+
]);
|
|
176
|
+
creationSignal.removeEventListener("abort", onCreationAbort);
|
|
177
|
+
if (creationOutcome.status === "aborted") {
|
|
178
|
+
onCreationUncertain();
|
|
179
|
+
void creation.then(
|
|
180
|
+
async handle => {
|
|
181
|
+
await settleTerminalCleanup(() => handle.kill());
|
|
182
|
+
await settleTerminalCleanup(() => handle.release());
|
|
183
|
+
},
|
|
184
|
+
() => undefined,
|
|
185
|
+
);
|
|
186
|
+
throw new Error("terminal creation aborted");
|
|
187
|
+
}
|
|
188
|
+
if (creationOutcome.status === "failed") {
|
|
189
|
+
onCreationUncertain();
|
|
190
|
+
throw creationOutcome.error;
|
|
191
|
+
}
|
|
192
|
+
const handle = creationOutcome.handle;
|
|
193
|
+
const publication = Promise.resolve().then(() =>
|
|
194
|
+
connection.sessionUpdate({
|
|
195
|
+
sessionId,
|
|
196
|
+
update: {
|
|
197
|
+
sessionUpdate: "tool_call_update",
|
|
198
|
+
toolCallId: params.toolCallId,
|
|
199
|
+
status: "in_progress",
|
|
200
|
+
content: [{ type: "terminal", terminalId: handle.id }],
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
const aborted = Promise.withResolvers<"aborted">();
|
|
205
|
+
const onAbort = () => aborted.resolve("aborted");
|
|
206
|
+
params.signal?.addEventListener("abort", onAbort, { once: true });
|
|
207
|
+
if (params.signal?.aborted) aborted.resolve("aborted");
|
|
208
|
+
const outcome = await Promise.race([
|
|
209
|
+
publication.then(
|
|
210
|
+
() => ({ status: "published" as const }),
|
|
211
|
+
error => ({ status: "failed" as const, error }),
|
|
212
|
+
),
|
|
213
|
+
Bun.sleep(ACP_TERMINAL_PUBLICATION_WAIT_MS).then(() => ({ status: "timeout" as const })),
|
|
214
|
+
aborted.promise.then(() => ({ status: "aborted" as const })),
|
|
215
|
+
]);
|
|
216
|
+
params.signal?.removeEventListener("abort", onAbort);
|
|
217
|
+
if (outcome.status !== "published") {
|
|
218
|
+
const killed = await settleTerminalCleanup(() => handle.kill());
|
|
219
|
+
const released = await settleTerminalCleanup(() => handle.release());
|
|
220
|
+
if (!params.signal?.aborted || !killed || !released) onPublicationFailure();
|
|
221
|
+
if (outcome.status === "failed") throw outcome.error;
|
|
222
|
+
throw new Error(
|
|
223
|
+
outcome.status === "aborted"
|
|
224
|
+
? "terminal publication aborted"
|
|
225
|
+
: `terminal publication did not settle within ${ACP_TERMINAL_PUBLICATION_WAIT_MS}ms`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return wrapTerminalHandle(handle, providerIdentity.leaseId, providerIdentity);
|
|
92
229
|
}
|
|
93
230
|
|
|
94
|
-
function wrapTerminalHandle(
|
|
231
|
+
function wrapTerminalHandle(
|
|
232
|
+
handle: AcpTerminalHandle,
|
|
233
|
+
providerLeaseId: string,
|
|
234
|
+
providerLeaseIdentity: ClientBridgeProviderLeaseIdentity,
|
|
235
|
+
): ClientBridgeTerminalHandle {
|
|
95
236
|
return {
|
|
96
237
|
terminalId: handle.id,
|
|
238
|
+
providerLeaseId,
|
|
239
|
+
providerLeaseIdentity,
|
|
97
240
|
async currentOutput() {
|
|
98
241
|
const out = await handle.currentOutput();
|
|
99
242
|
return {
|
|
@@ -56,6 +56,7 @@ export interface GjcBundleSettingsDependencies {
|
|
|
56
56
|
export interface GjcBundleSettingsCallbacks {
|
|
57
57
|
onClose: () => void;
|
|
58
58
|
onBundlesChanged?: () => void;
|
|
59
|
+
onRenderRequested?: () => void;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
export type GjcBundleSettingsState =
|
|
@@ -187,6 +188,7 @@ export class GjcBundleSettingsComponent extends Container {
|
|
|
187
188
|
),
|
|
188
189
|
);
|
|
189
190
|
this.addChild(new DynamicBorder());
|
|
191
|
+
this.callbacks.onRenderRequested?.();
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
#renderList(): void {
|
|
@@ -127,6 +127,7 @@ export interface PluginDetailCallbacks {
|
|
|
127
127
|
onFeatureChange: (feature: string, enabled: boolean) => void;
|
|
128
128
|
onConfigChange: (key: string, value: unknown) => void;
|
|
129
129
|
onBack: () => void;
|
|
130
|
+
onRenderRequested?: () => void;
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
/**
|
|
@@ -288,6 +289,7 @@ export class PluginDetailComponent extends Container {
|
|
|
288
289
|
this.addChild(new Spacer(1));
|
|
289
290
|
this.addChild(new Text(theme.fg("dim", " Enter to edit · Esc to go back"), 0, 0));
|
|
290
291
|
this.addChild(new DynamicBorder());
|
|
292
|
+
this.callbacks.onRenderRequested?.();
|
|
291
293
|
}
|
|
292
294
|
|
|
293
295
|
handleInput(data: string): void {
|
|
@@ -409,6 +411,7 @@ class ConfigInputSubmenu extends Container {
|
|
|
409
411
|
export interface PluginSettingsCallbacks {
|
|
410
412
|
onClose: () => void;
|
|
411
413
|
onPluginChanged: () => void;
|
|
414
|
+
onRenderRequested?: () => void;
|
|
412
415
|
}
|
|
413
416
|
|
|
414
417
|
/** Component with handleInput method */
|
|
@@ -450,6 +453,7 @@ export class PluginSettingsComponent extends Container {
|
|
|
450
453
|
});
|
|
451
454
|
|
|
452
455
|
this.addChild(this.#viewComponent);
|
|
456
|
+
this.callbacks.onRenderRequested?.();
|
|
453
457
|
}
|
|
454
458
|
|
|
455
459
|
#showPluginDetail(plugin: InstalledPlugin): void {
|
|
@@ -477,6 +481,7 @@ export class PluginSettingsComponent extends Container {
|
|
|
477
481
|
this.callbacks.onPluginChanged();
|
|
478
482
|
},
|
|
479
483
|
onBack: () => this.#showPluginList(),
|
|
484
|
+
onRenderRequested: () => this.callbacks.onRenderRequested?.(),
|
|
480
485
|
});
|
|
481
486
|
|
|
482
487
|
this.addChild(this.#viewComponent);
|
|
@@ -719,6 +719,8 @@ export interface SettingsCallbacks {
|
|
|
719
719
|
getStatusLinePreview?: (width?: number) => string;
|
|
720
720
|
/** Called when plugins change */
|
|
721
721
|
onPluginsChanged?: () => void;
|
|
722
|
+
/** Called when asynchronously rebuilt settings content needs a repaint. */
|
|
723
|
+
onRenderRequested?: () => void;
|
|
722
724
|
/** Called when an interactive setting cannot be committed. */
|
|
723
725
|
onError?: (message: string) => void;
|
|
724
726
|
/** Called when settings panel is closed */
|
|
@@ -1287,6 +1289,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1287
1289
|
this.#pluginComponent = new PluginSettingsComponent(this.context.cwd, {
|
|
1288
1290
|
onClose: () => this.callbacks.onCancel(),
|
|
1289
1291
|
onPluginChanged: () => this.callbacks.onPluginsChanged?.(),
|
|
1292
|
+
onRenderRequested: () => this.callbacks.onRenderRequested?.(),
|
|
1290
1293
|
});
|
|
1291
1294
|
this.addChild(this.#pluginComponent);
|
|
1292
1295
|
}
|
|
@@ -1296,6 +1299,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1296
1299
|
{
|
|
1297
1300
|
onClose: () => this.callbacks.onCancel(),
|
|
1298
1301
|
onBundlesChanged: () => this.callbacks.onPluginsChanged?.(),
|
|
1302
|
+
onRenderRequested: () => this.callbacks.onRenderRequested?.(),
|
|
1299
1303
|
},
|
|
1300
1304
|
{
|
|
1301
1305
|
runtimeSnapshotProvider: this.context.gjcRuntimeSnapshot,
|
|
@@ -1334,6 +1334,7 @@ export class SelectorController {
|
|
|
1334
1334
|
onPluginsChanged: () => {
|
|
1335
1335
|
this.ctx.ui.requestRender();
|
|
1336
1336
|
},
|
|
1337
|
+
onRenderRequested: () => this.ctx.ui.requestRender(),
|
|
1337
1338
|
onCancel: () => {
|
|
1338
1339
|
done();
|
|
1339
1340
|
// Restore status line to saved settings
|
|
@@ -65,5 +65,5 @@ Use it only for sanctioned GJC workflow CLI persistence or state read/write/cont
|
|
|
65
65
|
# Output minimizer
|
|
66
66
|
|
|
67
67
|
- Bash stdout/stderr may be rewritten before you see it: long output keeps only the last 1 KiB by default to reduce noise and input-token use. Explicit `tools.artifactTailBytes` / `tools.artifactHeadBytes` settings can set the tail budget or retain both ends. Prefer focused commands and dedicated `search`/`find` tools over producing broad output. Test/lint runners (e.g. `bun test`, `cargo test`, ESLint) are also passed through heuristic filters that drop noise and keep failures.
|
|
68
|
-
- When the local minimizer changes visible text, successful artifact storage appends a footer containing an `artifact://<id>` reference. Complete artifacts are labeled as full output; hard-capped artifacts
|
|
69
|
-
- ACP/client-terminal output can arrive already truncated from the beginning. Treat any truncation notice or metadata as evidence that the visible tail is incomplete. Recover omitted output only when an `artifact://` footer or metadata reference is present; truncation evidence without a reference means the visible output is incomplete and no artifact is recoverable. Output with neither truncation evidence nor an artifact reference is the complete emitted output.
|
|
68
|
+
- When the local minimizer changes visible text, successful artifact storage appends a footer containing an `artifact://<id>` reference. Artifact allocation is best-effort on direct, async, and monitor paths, and only decimal session artifact ids are accepted. Native callback delivery bounds both the core-to-native relay and N-API queue with backpressure and preserves a 64 KiB terminal tail after its core stream budget, emitting at most one typed aggregate loss marker. Complete artifacts are labeled as full output; hard-capped artifacts, streams that lost bytes before Bash capture, reader/client captures that cannot prove completeness, and loss counters beyond JavaScript's exact integer range report incompleteness instead. If artifact allocation/storage is unavailable before a writer/save operation is attempted, truncation may have no reference or diagnostic. If an artifact writer/save operation is attempted and fails, a bounded diagnostic is emitted without inventing an artifact URI.
|
|
69
|
+
- ACP/client-terminal output can arrive already truncated from the beginning. Terminal creation, kill, output-recovery, and release RPCs are settlement-bounded; stalled or failed exit/kill/output operations report source-capture uncertainty instead of hanging or claiming completeness. Treat any truncation notice or metadata as evidence that the visible tail is incomplete. Recover omitted output only when an `artifact://` footer or metadata reference is present; truncation evidence without a reference means the visible output is incomplete and no artifact is recoverable. Output with neither truncation evidence nor an artifact reference is the complete emitted output.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
Start a background monitor that streams events from a long-running script. Each stdout line is
|
|
1
|
+
Start a background monitor that streams events from a long-running script. Each stdout line is captured; persistent notifications are latest-biased and coalesced over a short debounce window, while terminal completion flushes the newest pending line. Events arrive on their own schedule and are not replies from the user, even if one lands while you're waiting for the user to answer a question.
|
|
2
2
|
|
|
3
3
|
Pick by how many notifications you need:
|
|
4
4
|
- **One** ("tell me when the server is ready / the build finishes") → use `bash` with `async: true`. That returns a single completion notification when the command exits.
|
|
5
|
-
- **Many ongoing events** (logs, polling, file watching) → use `monitor`. The script keeps running and
|
|
5
|
+
- **Many ongoing events** (logs, polling, file watching) → use `monitor`. The script keeps running and new stdout is captured; persistent notifications are coalesced so ordinary log traffic does not create one model turn per line.
|
|
6
6
|
|
|
7
7
|
`monitor` uses the same permission rules as `bash`. To stop a monitor, cancel its background task via `job` with the returned `task_id`, or end the session.
|
|
8
8
|
|
|
@@ -15,7 +15,7 @@ Pick by how many notifications you need:
|
|
|
15
15
|
|
|
16
16
|
## Inputs
|
|
17
17
|
|
|
18
|
-
- `command` (required): shell command to run as a background monitor.
|
|
18
|
+
- `command` (required): shell command to run as a background monitor. Stdout is captured line-by-line; persistent notifications are coalesced before delivery.
|
|
19
19
|
- `kind` (required): one of `"log"`, `"poll"`, `"watch"`, `"other"`. Describes the monitoring strategy so listings can surface useful categories.
|
|
20
20
|
- `description` (required): short human-readable description of what is being monitored. Appears in task listings.
|
|
21
21
|
- `timeout` (optional): maximum wall-clock seconds the monitor may run before automatic shutdown. Omit for the session lifetime.
|
|
@@ -23,7 +23,7 @@ Pick by how many notifications you need:
|
|
|
23
23
|
|
|
24
24
|
## Output
|
|
25
25
|
|
|
26
|
-
Returns `Monitor started · task <task_id>` plus a task entry visible via `job({list: true})`.
|
|
26
|
+
Returns `Monitor started · task <task_id>` plus a task entry visible via `job({list: true})`. Persistent notifications contain the latest line and the count of earlier coalesced lines; terminal completion flushes the newest pending line.
|
|
27
27
|
|
|
28
28
|
## Cancellation
|
|
29
29
|
|