@gajae-code/coding-agent 0.16.4 → 0.16.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 +54 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +7 -0
- package/dist/types/discovery/helpers.d.ts +33 -0
- package/dist/types/exec/bash-executor.d.ts +17 -1
- package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
- package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-signals.d.ts +2 -0
- package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
- package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
- package/dist/types/exec/bash-shell-worker.d.ts +1 -0
- package/dist/types/exec/isolated-shell.d.ts +19 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
- package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +46 -6
- package/dist/types/extensibility/extensions/types.d.ts +16 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -1
- package/dist/types/extensibility/skill-management.d.ts +9 -4
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +12 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
- package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
- package/dist/types/modes/components/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/broker/ensure.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
- package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
- package/dist/types/sdk/client/client.d.ts +6 -0
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/session-runtime.d.ts +4 -2
- package/dist/types/sdk/lifecycle/service.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +10 -2
- package/dist/types/sdk/session.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/setup/paseo/announce.d.ts +4 -3
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +9 -7
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +198 -14
- package/src/config/model-preset-registry.ts +115 -16
- package/src/config/model-registry.ts +16 -12
- package/src/coordinator-mcp/question-state.ts +33 -2
- package/src/coordinator-mcp/server.ts +477 -101
- package/src/crash/sanitize.ts +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +50 -62
- package/src/discovery/helpers.ts +297 -32
- package/src/exec/bash-executor.ts +330 -50
- package/src/exec/bash-shell-guardian.ts +388 -0
- package/src/exec/bash-shell-runtime-entry.ts +5 -0
- package/src/exec/bash-shell-signals.ts +29 -0
- package/src/exec/bash-shell-supervisor-entry.ts +3 -0
- package/src/exec/bash-shell-supervisor.ts +359 -0
- package/src/exec/bash-shell-worker-entry.ts +5 -0
- package/src/exec/bash-shell-worker-protocol.ts +36 -0
- package/src/exec/bash-shell-worker.ts +116 -0
- package/src/exec/isolated-shell.ts +393 -0
- package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
- package/src/extensibility/extensions/function-hooks.ts +959 -0
- package/src/extensibility/extensions/index.ts +22 -0
- package/src/extensibility/extensions/loader.ts +53 -2
- package/src/extensibility/extensions/runner.ts +895 -264
- package/src/extensibility/extensions/types.ts +42 -0
- package/src/extensibility/extensions/wrapper.ts +56 -15
- package/src/extensibility/gjc-plugins/compiler.ts +20 -2
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
- package/src/extensibility/gjc-plugins/schema.ts +121 -4
- package/src/extensibility/gjc-plugins/types.ts +25 -0
- package/src/extensibility/hooks/loader.ts +133 -16
- package/src/extensibility/hooks/types.ts +28 -0
- package/src/extensibility/runtime-skill-discovery.ts +80 -47
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +29 -9
- package/src/gjc-runtime/empty-delete-gc.ts +94 -4
- package/src/gjc-runtime/gc-runtime.ts +29 -0
- package/src/gjc-runtime/linux-proc.ts +14 -0
- package/src/gjc-runtime/session-state-lock.ts +51 -8
- package/src/gjc-runtime/session-state-sidecar.ts +27 -11
- package/src/gjc-runtime/state-writer.ts +30 -29
- package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
- package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -0
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/lsp/config.ts +10 -3
- package/src/lsp/index.ts +1 -1
- package/src/lsp/path-trust.ts +69 -14
- package/src/main.ts +6 -1
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/tool-status-header.ts +3 -1
- package/src/modes/controllers/command-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/interactive-mode.ts +6 -5
- package/src/modes/print-mode.ts +5 -1
- package/src/modes/prompt-action-autocomplete.ts +54 -27
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/manager.ts +20 -2
- package/src/runtime-mcp/pool.test.ts +20 -11
- package/src/sdk/broker/broker.ts +119 -28
- package/src/sdk/broker/ensure.ts +248 -41
- package/src/sdk/broker/lifecycle-ledger.ts +7 -17
- package/src/sdk/broker/lifecycle.ts +56 -6
- package/src/sdk/broker/startup-failure.ts +15 -6
- package/src/sdk/bus/chat-daemon-control.ts +3 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +35 -5
- package/src/sdk/bus/chat-effect-journal.ts +132 -7
- package/src/sdk/bus/conversation-store.ts +29 -0
- package/src/sdk/bus/lifecycle-commands.ts +17 -11
- package/src/sdk/bus/slack-conversation.ts +1 -0
- package/src/sdk/bus/slack-daemon.ts +387 -48
- package/src/sdk/bus/telegram-daemon.ts +16 -2
- package/src/sdk/bus/topic-registry.ts +19 -0
- package/src/sdk/cli/master-cli.ts +1 -1
- package/src/sdk/cli/session-cli.ts +2 -2
- package/src/sdk/client/client.ts +66 -17
- package/src/sdk/host/host.ts +29 -9
- package/src/sdk/host/query/revision-store.ts +19 -13
- package/src/sdk/host/reverse-leases.ts +7 -4
- package/src/sdk/host/session-runtime.test.ts +50 -8
- package/src/sdk/host/session-runtime.ts +158 -51
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session.ts +155 -26
- package/src/sdk/transport/relay.ts +35 -13
- package/src/session/agent-session.ts +117 -52
- package/src/session/fallback-chain-controller.ts +38 -0
- package/src/session/internal/managed-session-storage.ts +19 -5
- package/src/session/messages.ts +5 -0
- package/src/session/session-manager.ts +29 -5
- package/src/session-import/redact.ts +5 -1
- package/src/setup/paseo/announce.ts +49 -43
- package/src/setup/paseo/setup-deps.ts +1 -1
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/system-prompt.ts +25 -3
- package/src/task/executor.ts +5 -0
- package/src/task/index.ts +4 -0
- package/src/tools/bash.ts +28 -11
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +13 -2
- package/src/tools/read.ts +14 -2
- package/src/tools/render-utils.ts +1 -3
- package/src/tools/resource-gc.ts +28 -8
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +15 -0
- package/src/tools/subagent.ts +3 -2
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/tools/ultragoal-ask-guard.ts +2 -2
|
@@ -4821,7 +4821,11 @@ export class TelegramNotificationDaemon {
|
|
|
4821
4821
|
.catch(() => undefined);
|
|
4822
4822
|
}
|
|
4823
4823
|
};
|
|
4824
|
-
const
|
|
4824
|
+
const threadSessionId =
|
|
4825
|
+
threadId === undefined
|
|
4826
|
+
? undefined
|
|
4827
|
+
: this.topics.sessionForLifecycleTopic(String(threadId), String(this.opts.chatId));
|
|
4828
|
+
const parsed = parseLifecycleCommand(text, { ...commandCtx, threadSessionId });
|
|
4825
4829
|
if (parsed.kind === "none") return false;
|
|
4826
4830
|
if (updateId !== undefined && this.dispatchState.seenUpdateIds.has(updateId)) return true;
|
|
4827
4831
|
if (updateId !== undefined) await this.rememberSeenUpdateId(updateId);
|
|
@@ -4841,7 +4845,17 @@ export class TelegramNotificationDaemon {
|
|
|
4841
4845
|
? `Recent sessions could not be verified: ${recent.message}`
|
|
4842
4846
|
: recent.entries.length
|
|
4843
4847
|
? recent.entries
|
|
4844
|
-
.map(entry =>
|
|
4848
|
+
.map(entry => {
|
|
4849
|
+
const connected = this.#logicalSessionOwners.has(entry.sessionId);
|
|
4850
|
+
return [
|
|
4851
|
+
`${connected ? "🟢 connected" : "⚪ saved"} — ${code(entry.sessionId)}`,
|
|
4852
|
+
entry.title ? ` ${code(entry.title)}` : undefined,
|
|
4853
|
+
entry.path ? ` ${code(entry.path)}` : undefined,
|
|
4854
|
+
connected ? undefined : ` Resume: ${code(`/session_resume ${entry.sessionId}`)}`,
|
|
4855
|
+
]
|
|
4856
|
+
.filter((line): line is string => line !== undefined)
|
|
4857
|
+
.join("\n");
|
|
4858
|
+
})
|
|
4845
4859
|
.join("\n")
|
|
4846
4860
|
: "No recent sessions.";
|
|
4847
4861
|
await replyHtml(
|
|
@@ -775,6 +775,25 @@ export class TopicRegistry {
|
|
|
775
775
|
return this.byTopic.get(topicId);
|
|
776
776
|
}
|
|
777
777
|
|
|
778
|
+
/**
|
|
779
|
+
* Resolve a durable topic association for explicit lifecycle controls.
|
|
780
|
+
*
|
|
781
|
+
* Unlike inbound message routing, this may return an inactive session so a
|
|
782
|
+
* user can resume it from its old topic. Ambiguous, malformed, cross-chat,
|
|
783
|
+
* and quarantined records fail closed.
|
|
784
|
+
*/
|
|
785
|
+
sessionForLifecycleTopic(topicId: string, chatId: string): string | undefined {
|
|
786
|
+
if (!isValidTopicId(topicId)) return undefined;
|
|
787
|
+
const matches = [...this.topics].filter(
|
|
788
|
+
([, record]) =>
|
|
789
|
+
record.topicId === topicId &&
|
|
790
|
+
record.chatId === chatId &&
|
|
791
|
+
!record.bindingMalformed &&
|
|
792
|
+
record.authorityState !== "legacy_quarantined",
|
|
793
|
+
);
|
|
794
|
+
return matches.length === 1 ? matches[0]![0] : undefined;
|
|
795
|
+
}
|
|
796
|
+
|
|
778
797
|
/** All session ids with a persisted topic record. */
|
|
779
798
|
sessionIds(): string[] {
|
|
780
799
|
return [...this.topics.keys()];
|
|
@@ -15,7 +15,7 @@ import { SdkClientError } from "../client";
|
|
|
15
15
|
import { dispatchSpawnGlobal } from "../lifecycle/broker-client";
|
|
16
16
|
|
|
17
17
|
const MASTER_CAPABILITY_ENV = "GJC_MASTER_CAPABILITY";
|
|
18
|
-
const MASTER_SESSION_ENV = "
|
|
18
|
+
const MASTER_SESSION_ENV = "GJC_MASTER_OWNER_SESSION_ID";
|
|
19
19
|
const SPAWN_TIMEOUT_MS = 120_000;
|
|
20
20
|
|
|
21
21
|
export class SdkMasterCliError extends Error {
|
|
@@ -455,7 +455,7 @@ export async function runSdkSearch(
|
|
|
455
455
|
createService: (agentDir: string) => SessionLifecycleService = createBrokerSessionLifecycleService,
|
|
456
456
|
probe: (agentDir: string, result: SdkSearchResultV1) => Promise<SdkSearchResultV1> = probeSearchRows,
|
|
457
457
|
): Promise<{ result: SdkSearchResultV1; exitCode: 0 | 1 }> {
|
|
458
|
-
const agentDir = args.agentDir ?? getAgentDir();
|
|
458
|
+
const agentDir = path.resolve(args.agentDir ?? getAgentDir());
|
|
459
459
|
const locator = await resolveSessionLocator(args.repo ?? process.cwd(), agentDir);
|
|
460
460
|
const scope: ScopeNameV1 =
|
|
461
461
|
args.scope === undefined || args.scope === "repo" || args.scope === "pwd" || args.scope === "global"
|
|
@@ -1804,7 +1804,7 @@ export async function runSdkSessionCli(
|
|
|
1804
1804
|
"Expected one of: list, inspect, send, status, tail, retire, raw (control|query|global).",
|
|
1805
1805
|
2,
|
|
1806
1806
|
);
|
|
1807
|
-
const agentDir = args.agentDir ?? getAgentDir();
|
|
1807
|
+
const agentDir = path.resolve(args.agentDir ?? getAgentDir());
|
|
1808
1808
|
if (action === "list") {
|
|
1809
1809
|
writeOutput(stripSecretFields(await runList(agentDir, args)));
|
|
1810
1810
|
return;
|
package/src/sdk/client/client.ts
CHANGED
|
@@ -31,6 +31,14 @@ export class SdkClientError extends Error {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** A prepared transport operation failed before any wire handoff. */
|
|
35
|
+
export class SdkPreparedDispatchError extends SdkClientError {
|
|
36
|
+
constructor(error: SdkClientError) {
|
|
37
|
+
super(error.code, error.message, error.details, error.reconnect);
|
|
38
|
+
this.name = "SdkPreparedDispatchError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
export type SdkReconnectTerminationReason = "attempts_exhausted" | "deadline" | "cancelled";
|
|
35
43
|
|
|
36
44
|
/**
|
|
@@ -90,6 +98,8 @@ export interface SdkRequestOptions {
|
|
|
90
98
|
* alter that settlement.
|
|
91
99
|
*/
|
|
92
100
|
onDispatch?: SdkDispatchHandler;
|
|
101
|
+
/** Internal prepared-dispatch mode: fail pre-send instead of reconnecting. */
|
|
102
|
+
connectedOnly?: boolean;
|
|
93
103
|
}
|
|
94
104
|
export type SdkFrame = Record<string, unknown>;
|
|
95
105
|
/**
|
|
@@ -157,6 +167,7 @@ type Incarnation = {
|
|
|
157
167
|
};
|
|
158
168
|
type Pending = {
|
|
159
169
|
readonly incarnation: Incarnation;
|
|
170
|
+
readonly connectedOnly: boolean;
|
|
160
171
|
resolve: (value: unknown) => void;
|
|
161
172
|
reject: (error: Error) => void;
|
|
162
173
|
timer: NodeJS.Timeout;
|
|
@@ -453,11 +464,30 @@ export class SdkClient {
|
|
|
453
464
|
}
|
|
454
465
|
|
|
455
466
|
async #request(frame: Frame, options: SdkRequestOptions, onResponse?: () => void): Promise<unknown> {
|
|
456
|
-
if (this.#closed)
|
|
457
|
-
|
|
458
|
-
|
|
467
|
+
if (this.#closed) {
|
|
468
|
+
const error = new SdkClientError("connection_closed", "SDK client closed");
|
|
469
|
+
throw options.connectedOnly ? new SdkPreparedDispatchError(error) : error;
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
this.#throwIfDeadlineElapsed();
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (options.connectedOnly && error instanceof SdkClientError) throw new SdkPreparedDispatchError(error);
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
let incarnation: Incarnation;
|
|
478
|
+
if (options.connectedOnly) {
|
|
479
|
+
try {
|
|
480
|
+
incarnation = this.#requireConnectedIncarnation();
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (error instanceof SdkClientError) throw new SdkPreparedDispatchError(error);
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
} else incarnation = await this.#connect();
|
|
459
486
|
const timeoutMs = this.#remainingTimeout(options.timeoutMs ?? this.#timeoutMs);
|
|
460
|
-
if (timeoutMs <= 0)
|
|
487
|
+
if (timeoutMs <= 0) {
|
|
488
|
+
const error = this.#deadlineError();
|
|
489
|
+
throw options.connectedOnly ? new SdkPreparedDispatchError(error) : error;
|
|
490
|
+
}
|
|
461
491
|
const id = randomUUID();
|
|
462
492
|
const requestFrame = {
|
|
463
493
|
...frame,
|
|
@@ -482,6 +512,7 @@ export class SdkClient {
|
|
|
482
512
|
const deferred = Promise.withResolvers<unknown>();
|
|
483
513
|
const pending: Pending = {
|
|
484
514
|
incarnation,
|
|
515
|
+
connectedOnly: options.connectedOnly === true,
|
|
485
516
|
resolve: deferred.resolve,
|
|
486
517
|
reject: deferred.reject,
|
|
487
518
|
sent: false,
|
|
@@ -502,7 +533,8 @@ export class SdkClient {
|
|
|
502
533
|
};
|
|
503
534
|
this.#pending.set(id, pending);
|
|
504
535
|
if (!this.#isActive(incarnation) || incarnation.socket.readyState !== WebSocket.OPEN) {
|
|
505
|
-
|
|
536
|
+
const error = new SdkClientError("unavailable", "SDK WebSocket is not connected");
|
|
537
|
+
this.#settlePending(id, pending, options.connectedOnly ? new SdkPreparedDispatchError(error) : error);
|
|
506
538
|
return await deferred.promise;
|
|
507
539
|
}
|
|
508
540
|
if (options.beforeDispatch) {
|
|
@@ -553,16 +585,22 @@ export class SdkClient {
|
|
|
553
585
|
this.#settlePending(
|
|
554
586
|
id,
|
|
555
587
|
pending,
|
|
556
|
-
|
|
588
|
+
options.connectedOnly
|
|
589
|
+
? new SdkPreparedDispatchError(
|
|
590
|
+
new SdkClientError("connection_closed", "SDK client closed during dispatch"),
|
|
591
|
+
)
|
|
592
|
+
: new SdkClientError("connection_closed", "SDK client closed during dispatch"),
|
|
557
593
|
);
|
|
558
594
|
return await deferred.promise;
|
|
559
595
|
}
|
|
560
596
|
if (this.#deadline !== undefined && Date.now() >= this.#deadline) {
|
|
561
|
-
|
|
597
|
+
const error = this.#deadlineError();
|
|
598
|
+
this.#settlePending(id, pending, options.connectedOnly ? new SdkPreparedDispatchError(error) : error);
|
|
562
599
|
return await deferred.promise;
|
|
563
600
|
}
|
|
564
601
|
if (incarnation.socket.readyState !== WebSocket.OPEN) {
|
|
565
|
-
|
|
602
|
+
const error = new SdkClientError("unavailable", "SDK WebSocket is not connected");
|
|
603
|
+
this.#settlePending(id, pending, options.connectedOnly ? new SdkPreparedDispatchError(error) : error);
|
|
566
604
|
return await deferred.promise;
|
|
567
605
|
}
|
|
568
606
|
// Handoff bookkeeping is reentrancy-safe: `sent` flips BEFORE the wire
|
|
@@ -593,12 +631,14 @@ export class SdkClient {
|
|
|
593
631
|
// in which case that settlement stands and must not be displaced.
|
|
594
632
|
pending.sent = false;
|
|
595
633
|
this.#sentRecords.delete(id);
|
|
634
|
+
const sendError =
|
|
635
|
+
error instanceof SdkClientError
|
|
636
|
+
? error
|
|
637
|
+
: new SdkClientError("unavailable", "SDK WebSocket send failed", error);
|
|
596
638
|
this.#settlePending(
|
|
597
639
|
id,
|
|
598
640
|
pending,
|
|
599
|
-
|
|
600
|
-
? error
|
|
601
|
-
: new SdkClientError("unavailable", "SDK WebSocket send failed", error),
|
|
641
|
+
options.connectedOnly ? new SdkPreparedDispatchError(sendError) : sendError,
|
|
602
642
|
);
|
|
603
643
|
}
|
|
604
644
|
return await deferred.promise;
|
|
@@ -649,6 +689,13 @@ export class SdkClient {
|
|
|
649
689
|
return await cycle.promise!;
|
|
650
690
|
}
|
|
651
691
|
|
|
692
|
+
#requireConnectedIncarnation(): Incarnation {
|
|
693
|
+
const current = this.#currentSocketRecord;
|
|
694
|
+
if (!current || !this.#isActive(current) || current.socket.readyState !== WebSocket.OPEN)
|
|
695
|
+
throw new SdkClientError("connection_closed", "SDK WebSocket disconnected before prepared dispatch.");
|
|
696
|
+
return current;
|
|
697
|
+
}
|
|
698
|
+
|
|
652
699
|
async #openWithRetry(cycle: Cycle): Promise<Incarnation> {
|
|
653
700
|
const startedAt = Date.now();
|
|
654
701
|
let attemptsConsumed = 0;
|
|
@@ -983,12 +1030,14 @@ export class SdkClient {
|
|
|
983
1030
|
clearTimeout(pending.timer);
|
|
984
1031
|
if (responseReceived) pending.onResponse?.();
|
|
985
1032
|
if (result instanceof Error) {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
pending.
|
|
1033
|
+
const errorResult =
|
|
1034
|
+
!pending.sent &&
|
|
1035
|
+
pending.connectedOnly &&
|
|
989
1036
|
result instanceof SdkClientError &&
|
|
990
|
-
(result
|
|
991
|
-
|
|
1037
|
+
!(result instanceof SdkPreparedDispatchError)
|
|
1038
|
+
? new SdkPreparedDispatchError(result)
|
|
1039
|
+
: result;
|
|
1040
|
+
if (transportFailure && pending.sent && errorResult instanceof SdkClientError)
|
|
992
1041
|
pending.reject(
|
|
993
1042
|
new SdkClientError(
|
|
994
1043
|
"uncertain_after_send",
|
|
@@ -998,7 +1047,7 @@ export class SdkClient {
|
|
|
998
1047
|
);
|
|
999
1048
|
else {
|
|
1000
1049
|
this.#sentRecords.delete(id);
|
|
1001
|
-
pending.reject(
|
|
1050
|
+
pending.reject(errorResult);
|
|
1002
1051
|
}
|
|
1003
1052
|
return;
|
|
1004
1053
|
}
|
package/src/sdk/host/host.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { logger } from "@gajae-code/utils";
|
|
2
2
|
import { AUTOROUTING_INACTIVE_WARNING } from "../../config/autorouting-contract";
|
|
3
|
+
import { FileLockAcquireError } from "../../config/file-lock";
|
|
3
4
|
import { redactBrokerRuntimeCapabilities, redactObservedRequestContent } from "./control/runtime-gate";
|
|
4
5
|
import { type EventFrame, SessionEventStream } from "./events";
|
|
5
6
|
import { isAutoroutingInactive } from "./internal-autorouting-state";
|
|
@@ -363,10 +364,10 @@ export class SessionSdkHost {
|
|
|
363
364
|
return "activated";
|
|
364
365
|
}
|
|
365
366
|
|
|
366
|
-
async stop(): Promise<"stopped" | "already"> {
|
|
367
|
+
async stop(options: { allowLockContention?: boolean } = {}): Promise<"stopped" | "already"> {
|
|
367
368
|
if (this.#stopPromise) return this.#stopPromise;
|
|
368
369
|
if (!this.#started) return "already";
|
|
369
|
-
const stopPromise = this.#stopStartedHost();
|
|
370
|
+
const stopPromise = this.#stopStartedHost(options);
|
|
370
371
|
this.#stopPromise = stopPromise;
|
|
371
372
|
try {
|
|
372
373
|
return await stopPromise;
|
|
@@ -375,18 +376,37 @@ export class SessionSdkHost {
|
|
|
375
376
|
}
|
|
376
377
|
}
|
|
377
378
|
|
|
378
|
-
async #stopStartedHost(): Promise<"stopped"> {
|
|
379
|
+
async #stopStartedHost(options: { allowLockContention?: boolean }): Promise<"stopped"> {
|
|
379
380
|
// Fence before the first await: everything after this point is teardown,
|
|
380
381
|
// and no in-flight activation may publish readiness across it.
|
|
381
382
|
this.#stopping = true;
|
|
382
383
|
this.#unsubscribe?.();
|
|
383
384
|
this.#unsubscribe = undefined;
|
|
384
|
-
if (this.#registration?.writer.unregister)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
385
|
+
if (this.#registration?.writer.unregister) {
|
|
386
|
+
try {
|
|
387
|
+
await this.#registration.writer.unregister({
|
|
388
|
+
sessionId: this.#options.sessionId,
|
|
389
|
+
stateRoot: this.#options.stateRoot,
|
|
390
|
+
endpointGeneration: this.events.generation,
|
|
391
|
+
});
|
|
392
|
+
} catch (error) {
|
|
393
|
+
// A live broker may hold the shared session-index lock beyond the
|
|
394
|
+
// bounded acquisition budget. Unregistration is best effort: leaving
|
|
395
|
+
// the durable row in place is fail-closed because peers still fence
|
|
396
|
+
// authority on process liveness and incarnation, while propagating the
|
|
397
|
+
// contention error turns ordinary shutdown into an uncaught failure.
|
|
398
|
+
if (
|
|
399
|
+
!options.allowLockContention ||
|
|
400
|
+
!(error instanceof FileLockAcquireError) ||
|
|
401
|
+
error.code !== "acquire_timeout"
|
|
402
|
+
)
|
|
403
|
+
throw error;
|
|
404
|
+
logger.warn("sdk broker unregister deferred because the session index is busy", {
|
|
405
|
+
holder: error.holder,
|
|
406
|
+
lockPath: error.lockPath,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
390
410
|
this.#started = false;
|
|
391
411
|
this.#stopping = false;
|
|
392
412
|
return "stopped";
|
|
@@ -397,20 +397,29 @@ export class RevisionStore {
|
|
|
397
397
|
if (!revision) return undefined;
|
|
398
398
|
revision.lastAccessed = this.now();
|
|
399
399
|
if (!revision.index?.items) return undefined;
|
|
400
|
-
const
|
|
400
|
+
const ranges = revision.index.items.slice(offset);
|
|
401
|
+
let count = 0;
|
|
401
402
|
let itemsBytes = 2; // []
|
|
402
|
-
for (const range of
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
if (candidateBytes > targetBytes && items.length) break;
|
|
403
|
+
for (const range of ranges) {
|
|
404
|
+
// Indexed ranges contain canonical JSON, so their byte lengths select
|
|
405
|
+
// the page without repeatedly loading and hashing the same chunk.
|
|
406
|
+
const itemBytes = range.end - range.start;
|
|
407
|
+
if (itemBytes > targetBytes) break;
|
|
408
|
+
const candidateBytes = itemsBytes + itemBytes + (count ? 1 : 0);
|
|
409
|
+
if (candidateBytes > targetBytes && count) break;
|
|
410
410
|
if (candidateBytes > 1024 * 1024) break;
|
|
411
|
-
|
|
411
|
+
count++;
|
|
412
412
|
itemsBytes = candidateBytes;
|
|
413
413
|
}
|
|
414
|
+
const items: unknown[] = [];
|
|
415
|
+
if (count) {
|
|
416
|
+
const start = ranges[0]!.start;
|
|
417
|
+
const bytes = await this.#readBytes(revision, start, ranges[count - 1]!.end);
|
|
418
|
+
for (let index = 0; index < count; index++) {
|
|
419
|
+
const range = ranges[index]!;
|
|
420
|
+
items.push(JSON.parse(bytes.subarray(range.start - start, range.end - start).toString("utf8")));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
414
423
|
return { items, complete: offset + items.length >= revision.index.items.length };
|
|
415
424
|
}
|
|
416
425
|
/** Returns canonical root JSON in a UTF-8-safe bounded range without parsing it. */
|
|
@@ -1095,9 +1104,6 @@ export class RevisionStore {
|
|
|
1095
1104
|
}
|
|
1096
1105
|
return Buffer.concat(values);
|
|
1097
1106
|
}
|
|
1098
|
-
async #readRange(revision: Revision, range: { start: number; end: number }): Promise<string> {
|
|
1099
|
-
return (await this.#readBytes(revision, range.start, range.end)).toString("utf8");
|
|
1100
|
-
}
|
|
1101
1107
|
|
|
1102
1108
|
async #readChunks(chunks: string[], lengths: number[]): Promise<Buffer | undefined> {
|
|
1103
1109
|
if (!this.#directory || chunks.length === 0) return undefined;
|
|
@@ -89,7 +89,7 @@ export class ReverseLeaseRuntime {
|
|
|
89
89
|
readonly #onDefinitionsRemoved?: ReverseLeaseOptions["onDefinitionsRemoved"];
|
|
90
90
|
readonly #onCancel?: ReverseLeaseOptions["onCancel"];
|
|
91
91
|
readonly #leases = new Map<string, ProviderLease>();
|
|
92
|
-
readonly #idempotency = new Map<string, { fingerprint: string; lease: ProviderLease }>();
|
|
92
|
+
readonly #idempotency = new Map<string, { connectionId: string; fingerprint: string; lease: ProviderLease }>();
|
|
93
93
|
readonly #outstanding = new Map<string, Outstanding>();
|
|
94
94
|
readonly #installedCapabilities = new Set<string>();
|
|
95
95
|
readonly #sweepTimer: ReturnType<typeof setInterval>;
|
|
@@ -139,7 +139,7 @@ export class ReverseLeaseRuntime {
|
|
|
139
139
|
active: true,
|
|
140
140
|
};
|
|
141
141
|
this.#leases.set(capability, lease);
|
|
142
|
-
if (idempotencyKey) this.#idempotency.set(key, { fingerprint, lease });
|
|
142
|
+
if (idempotencyKey) this.#idempotency.set(key, { connectionId, fingerprint, lease });
|
|
143
143
|
return { ...lease };
|
|
144
144
|
}
|
|
145
145
|
const reclaiming =
|
|
@@ -152,7 +152,7 @@ export class ReverseLeaseRuntime {
|
|
|
152
152
|
existing!.expiresAt = now + this.#leaseTtlMs;
|
|
153
153
|
existing!.graceUntil = undefined;
|
|
154
154
|
existing!.active = true;
|
|
155
|
-
if (idempotencyKey) this.#idempotency.set(key, { fingerprint, lease: existing! });
|
|
155
|
+
if (idempotencyKey) this.#idempotency.set(key, { connectionId, fingerprint, lease: existing! });
|
|
156
156
|
return { ...existing! };
|
|
157
157
|
}
|
|
158
158
|
this.#installDefinitionsFor(capability, definitions);
|
|
@@ -165,7 +165,7 @@ export class ReverseLeaseRuntime {
|
|
|
165
165
|
active: true,
|
|
166
166
|
};
|
|
167
167
|
this.#leases.set(capability, lease);
|
|
168
|
-
if (idempotencyKey) this.#idempotency.set(key, { fingerprint, lease });
|
|
168
|
+
if (idempotencyKey) this.#idempotency.set(key, { connectionId, fingerprint, lease });
|
|
169
169
|
return { ...lease };
|
|
170
170
|
}
|
|
171
171
|
|
|
@@ -197,6 +197,9 @@ export class ReverseLeaseRuntime {
|
|
|
197
197
|
|
|
198
198
|
disconnect(connectionId: string): void {
|
|
199
199
|
const now = this.#now();
|
|
200
|
+
for (const [key, entry] of this.#idempotency) {
|
|
201
|
+
if (entry.connectionId === connectionId) this.#idempotency.delete(key);
|
|
202
|
+
}
|
|
200
203
|
for (const lease of this.#leases.values())
|
|
201
204
|
if (lease.connectionId === connectionId) {
|
|
202
205
|
lease.expiresAt = now;
|
|
@@ -5261,7 +5261,7 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
5261
5261
|
}
|
|
5262
5262
|
});
|
|
5263
5263
|
|
|
5264
|
-
test("a later provider error
|
|
5264
|
+
test("a later provider error never changes or re-opens an already terminal prompt", async () => {
|
|
5265
5265
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-once-"));
|
|
5266
5266
|
try {
|
|
5267
5267
|
const inflight = Promise.withResolvers<void>();
|
|
@@ -5282,13 +5282,8 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
5282
5282
|
// A lifecycle frame arriving after the terminal must not resurrect the record either.
|
|
5283
5283
|
await harness.emit("agent_start");
|
|
5284
5284
|
const settled = await harness.query("turn.prompt_status", { commandId, turnId });
|
|
5285
|
-
//
|
|
5286
|
-
|
|
5287
|
-
const { error: _claimedReason, ...claimedTerminal } = claimed;
|
|
5288
|
-
const { error: _lateReason, ...settledTerminal } = settled.result ?? {};
|
|
5289
|
-
expect(settledTerminal).toEqual(claimedTerminal);
|
|
5290
|
-
// The recorded reason is the sanitized late failure: never fabricated, never raw.
|
|
5291
|
-
expect(settled.result?.error).toEqual({ code: "upstream_error", message: "Prompt submission failed." });
|
|
5285
|
+
// A rejected submission after terminal publication is only a cleanup diagnostic.
|
|
5286
|
+
expect(settled.result).toEqual(claimed);
|
|
5292
5287
|
await harness.stop();
|
|
5293
5288
|
} finally {
|
|
5294
5289
|
await Bun.sleep(50);
|
|
@@ -7145,3 +7140,50 @@ test("SDK-only host rebinds the steering snapshot when the requester's turn wins
|
|
|
7145
7140
|
await rm(cwd, { recursive: true, force: true });
|
|
7146
7141
|
}
|
|
7147
7142
|
});
|
|
7143
|
+
|
|
7144
|
+
describe("rejection terminalization idempotency", () => {
|
|
7145
|
+
test("cleanup rejection after success preserves the sole correlated wire terminal and durable result", async () => {
|
|
7146
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-cleanup-rejection-"));
|
|
7147
|
+
const cleanup = Promise.withResolvers<void>();
|
|
7148
|
+
const harness = await invocationHarness("cleanup-rejection", cwd, {
|
|
7149
|
+
sendUserMessage: async (_content, options) => {
|
|
7150
|
+
await options?.onPreflightAcceptCommit?.();
|
|
7151
|
+
await cleanup.promise;
|
|
7152
|
+
},
|
|
7153
|
+
});
|
|
7154
|
+
const warning = spyOn(logger, "warn").mockImplementation(() => {});
|
|
7155
|
+
try {
|
|
7156
|
+
const accepted = await harness.control("turn.prompt", { text: "complete before cleanup" });
|
|
7157
|
+
expect(accepted.ok).toBe(true);
|
|
7158
|
+
const ids = { commandId: accepted.result?.commandId, turnId: accepted.result?.turnId };
|
|
7159
|
+
await harness.emit("agent_start");
|
|
7160
|
+
await harness.emit("agent_end", {
|
|
7161
|
+
messages: [{ role: "assistant", content: [{ type: "text", text: "Completed." }], stopReason: "stop" }],
|
|
7162
|
+
});
|
|
7163
|
+
const before = await settledStatus(harness, "turn.result", { kind: "prompt", ...ids });
|
|
7164
|
+
expect(before.status).toBe("terminal_ok");
|
|
7165
|
+
cleanup.reject(new Error("post-turn cleanup failed"));
|
|
7166
|
+
await Bun.sleep(50);
|
|
7167
|
+
const after = await harness.query("turn.result", { kind: "prompt", ...ids });
|
|
7168
|
+
expect(after.result).toEqual(before);
|
|
7169
|
+
expect(warning).toHaveBeenCalledWith("SDK submission cleanup failed after terminal publication", {
|
|
7170
|
+
kind: "prompt",
|
|
7171
|
+
...ids,
|
|
7172
|
+
error: { code: "internal", message: "Prompt submission failed." },
|
|
7173
|
+
});
|
|
7174
|
+
const correlated = harness.broadcasts.filter(
|
|
7175
|
+
frame => (frame.payload as { commandId?: string } | undefined)?.commandId === ids.commandId,
|
|
7176
|
+
);
|
|
7177
|
+
expect(correlated.filter(frame => frame.kind === "agent_failed")).toHaveLength(0);
|
|
7178
|
+
const terminals = correlated.filter(frame => frame.kind === "agent_end");
|
|
7179
|
+
expect(terminals).toHaveLength(1);
|
|
7180
|
+
expect(terminals[0]).toMatchObject({
|
|
7181
|
+
payload: { ...ids, outcome: (before as { outcome?: unknown }).outcome },
|
|
7182
|
+
});
|
|
7183
|
+
} finally {
|
|
7184
|
+
warning.mockRestore();
|
|
7185
|
+
await harness.stop();
|
|
7186
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7187
|
+
}
|
|
7188
|
+
});
|
|
7189
|
+
});
|