@gajae-code/coding-agent 0.18.11 → 0.18.12
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 +293 -0
- package/dist/types/async/job-manager.d.ts +7 -3
- package/dist/types/async/result-formatting.d.ts +2 -0
- package/dist/types/autoresearch/git.d.ts +1 -1
- package/dist/types/capability/fs.d.ts +2 -2
- package/dist/types/cli/accounts-cli.d.ts +2 -2
- package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
- package/dist/types/cli/install-activation.d.ts +10 -4
- package/dist/types/cli/public-command-errors.d.ts +1 -0
- package/dist/types/cli/update-cli.d.ts +14 -1
- package/dist/types/cli-ordinary.d.ts +2 -2
- package/dist/types/commands/accounts.d.ts +2 -1
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +5 -0
- package/dist/types/commit/changelog/index.d.ts +1 -0
- package/dist/types/config/autorouting-tier-map.d.ts +30 -0
- package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
- package/dist/types/config/config-hot-reload.d.ts +39 -0
- package/dist/types/config/file-lock.d.ts +8 -1
- package/dist/types/config/model-discovery-manager.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -6
- package/dist/types/config/model-registry.d.ts +56 -5
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +46 -3
- package/dist/types/config/settings.d.ts +33 -1
- package/dist/types/dap/config.d.ts +8 -4
- package/dist/types/debug/system-info.d.ts +6 -1
- package/dist/types/discovery/mcp-compat.d.ts +3 -1
- package/dist/types/edit/modes/patch.d.ts +5 -0
- package/dist/types/edit/modes/replace.d.ts +3 -0
- package/dist/types/exec/bash-executor.d.ts +12 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
- package/dist/types/migrate/adapters/codex.d.ts +2 -1
- package/dist/types/migrate/adapters/index.d.ts +10 -1
- package/dist/types/migrate/executor.d.ts +3 -1
- package/dist/types/migrate/types.d.ts +15 -3
- package/dist/types/modes/components/settings-selector.d.ts +6 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
- package/dist/types/runtime-mcp/manager.d.ts +7 -0
- package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
- package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
- package/dist/types/sdk/broker/broker.d.ts +7 -1
- package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
- package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
- package/dist/types/sdk/broker/ensure.d.ts +136 -7
- package/dist/types/sdk/broker/hop.d.ts +36 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
- package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
- package/dist/types/sdk/broker/session-index.d.ts +4 -1
- package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +23 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/sdk/cli/session-cli.d.ts +0 -1
- package/dist/types/sdk/host/session-runtime.d.ts +32 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
- package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
- package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +58 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
- package/dist/types/session/messages.d.ts +5 -1
- package/dist/types/session/session-manager.d.ts +13 -3
- package/dist/types/session/startup-auth-config.d.ts +24 -2
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/session/terminal-abort.d.ts +3 -1
- package/dist/types/session-import/service.d.ts +1 -1
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
- package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
- package/dist/types/task/executor.d.ts +19 -2
- package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
- package/dist/types/tools/atomic-file-write.d.ts +9 -7
- package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
- package/dist/types/tools/descriptors.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/python.d.ts +1 -1
- package/dist/types/tools/report-issue.d.ts +41 -0
- package/dist/types/tools/subagent.d.ts +23 -0
- package/dist/types/web/scrapers/types.d.ts +8 -1
- package/dist/types/web/search/providers/insane.d.ts +7 -1
- package/dist/types/web/search/providers/searxng.d.ts +5 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +2 -1
- package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
- package/scripts/generate-docs-index.ts +8 -1
- package/scripts/generate-sdk-operation-inventory.ts +22 -4
- package/src/async/job-manager.ts +356 -56
- package/src/async/result-formatting.ts +40 -0
- package/src/autoresearch/git.ts +9 -2
- package/src/autoresearch/prompts/prompt-setup.md +1 -1
- package/src/capability/fs.ts +27 -19
- package/src/capability/index.ts +2 -2
- package/src/cli/accounts-cli.ts +176 -8
- package/src/cli/auth-broker-cli.ts +21 -11
- package/src/cli/doctor/install-repairs.ts +35 -5
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/install-activation.ts +47 -4
- package/src/cli/migrate-cli.ts +7 -5
- package/src/cli/public-command-errors.ts +4 -0
- package/src/cli/root-flags.ts +2 -2
- package/src/cli/stats-cli.ts +9 -5
- package/src/cli/update-cli.ts +224 -7
- package/src/cli-ordinary.ts +7 -2
- package/src/commands/accounts.ts +10 -5
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +199 -42
- package/src/commands/ultragoal.ts +16 -8
- package/src/commit/changelog/index.ts +39 -1
- package/src/commit/message.ts +10 -3
- package/src/config/autorouting-tier-map.ts +30 -0
- package/src/config/config-hot-reload-trust.ts +148 -0
- package/src/config/config-hot-reload.ts +734 -0
- package/src/config/file-lock.ts +216 -56
- package/src/config/model-bindings-applier.ts +85 -47
- package/src/config/model-discovery-manager.ts +10 -0
- package/src/config/model-profile-activation.ts +716 -113
- package/src/config/model-registry.ts +1334 -158
- package/src/config/model-resolver.ts +3 -1
- package/src/config/models-config-schema.ts +6 -0
- package/src/config/settings-schema.ts +60 -3
- package/src/config/settings.ts +458 -24
- package/src/coordinator-mcp/policy.ts +26 -1
- package/src/crash/sanitize.ts +4 -0
- package/src/customization/import.ts +3 -1
- package/src/dap/config.ts +31 -11
- package/src/dap/defaults.json +1 -1
- package/src/debug/crash-diagnostics.ts +2 -1
- package/src/debug/report-bundle.ts +19 -0
- package/src/debug/system-info.ts +8 -3
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
- package/src/discovery/agents-md.ts +1 -1
- package/src/discovery/helpers.ts +1 -2
- package/src/discovery/mcp-compat.ts +120 -17
- package/src/edit/apply-patch/index.ts +12 -1
- package/src/edit/index.ts +13 -17
- package/src/edit/modes/patch.ts +187 -8
- package/src/edit/modes/replace.ts +136 -0
- package/src/eval/js/context-manager.ts +29 -3
- package/src/eval/js/worker-entry.ts +51 -18
- package/src/eval/py/executor.ts +14 -4
- package/src/eval/py/runner.py +144 -5
- package/src/exec/bash-executor.ts +81 -4
- package/src/exec/bash-shell-guardian.ts +4 -1
- package/src/exec/bash-shell-supervisor.ts +8 -4
- package/src/exec/isolated-shell.ts +5 -1
- package/src/export/html/index.ts +46 -6
- package/src/extensibility/custom-tools/wrapper.ts +3 -1
- package/src/extensibility/extensions/loader.ts +8 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
- package/src/gjc-runtime/gc-runtime.ts +33 -6
- package/src/gjc-runtime/launch-tmux.ts +7 -2
- package/src/gjc-runtime/launch-worktree.test.ts +148 -0
- package/src/gjc-runtime/launch-worktree.ts +101 -14
- package/src/gjc-runtime/managed-owner-admission.ts +46 -2
- package/src/gjc-runtime/session-resolution.ts +38 -6
- package/src/gjc-runtime/session-state-lock.ts +59 -14
- package/src/gjc-runtime/state-runtime.ts +23 -19
- package/src/hooks/native-skill-hook.ts +14 -11
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/local-protocol.ts +142 -8
- package/src/lsp/index.ts +29 -0
- package/src/lsp/workspace-path.ts +23 -0
- package/src/migrate/action-planner.ts +57 -0
- package/src/migrate/adapters/claude-code.ts +23 -4
- package/src/migrate/adapters/codex.ts +22 -4
- package/src/migrate/adapters/index.ts +22 -4
- package/src/migrate/adapters/opencode.ts +6 -1
- package/src/migrate/executor.ts +84 -7
- package/src/migrate/report.ts +19 -2
- package/src/migrate/types.ts +14 -4
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/permission-mode.ts +18 -1
- package/src/modes/components/assistant-message.ts +55 -1
- package/src/modes/components/settings-selector.ts +103 -37
- package/src/modes/controllers/command-controller.ts +38 -2
- package/src/modes/controllers/plan-mode-controller.ts +17 -7
- package/src/modes/controllers/selector-controller.ts +62 -6
- package/src/modes/interactive-mode.ts +68 -0
- package/src/modes/theme/theme.ts +15 -0
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/report-issue.md +13 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-credential-selector.ts +2 -2
- package/src/runtime-mcp/content-limits.ts +7 -0
- package/src/runtime-mcp/manager-pool.test.ts +5 -4
- package/src/runtime-mcp/manager.ts +32 -0
- package/src/runtime-mcp/oauth-flow.ts +32 -6
- package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
- package/src/runtime-mcp/render.ts +16 -1
- package/src/runtime-mcp/tool-bridge.ts +126 -17
- package/src/sdk/broker/broker-exit.ts +8 -5
- package/src/sdk/broker/broker.ts +342 -82
- package/src/sdk/broker/daemon-entry.ts +338 -116
- package/src/sdk/broker/doctor-restart.ts +2 -1
- package/src/sdk/broker/ensure.ts +1143 -265
- package/src/sdk/broker/hop.ts +274 -0
- package/src/sdk/broker/lifecycle-ledger.ts +35 -11
- package/src/sdk/broker/lock-host-identity.ts +29 -0
- package/src/sdk/broker/session-index.ts +62 -12
- package/src/sdk/bus/chat-command-policy.ts +16 -3
- package/src/sdk/bus/index.ts +52 -21
- package/src/sdk/bus/reconciliation-store.ts +201 -8
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/cli/session-cli.ts +21 -6
- package/src/sdk/host/session-runtime.test.ts +118 -23
- package/src/sdk/host/session-runtime.ts +352 -47
- package/src/sdk/lifecycle/service.ts +1 -0
- package/src/sdk/prompt-deadline-flush.ts +78 -5
- package/src/sdk/prompt-deadline-manager.ts +68 -14
- package/src/sdk/protocol/adapter-validation.ts +53 -11
- package/src/sdk/protocol/operation-inventory.generated.json +101 -2
- package/src/sdk/reconciliation-extensions.ts +1 -0
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +114 -61
- package/src/session/agent-session.ts +1043 -81
- package/src/session/internal/managed-session-scope.ts +126 -39
- package/src/session/internal/managed-session-storage.ts +14 -11
- package/src/session/internal/native-publish-outcome.ts +5 -1
- package/src/session/messages.ts +47 -6
- package/src/session/session-manager.ts +236 -14
- package/src/session/startup-auth-config.ts +116 -8
- package/src/session/streaming-output.ts +262 -4
- package/src/session/terminal-abort.ts +14 -2
- package/src/session-import/service.ts +1 -0
- package/src/skill-state/workflow-mutation-guard.ts +491 -32
- package/src/slash-commands/builtin-registry.ts +15 -3
- package/src/slash-commands/helpers/credential-switch.ts +10 -5
- package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
- package/src/task/executor.ts +103 -12
- package/src/task/index.ts +389 -175
- package/src/tools/ast-edit-write-path.ts +64 -0
- package/src/tools/ast-edit.ts +18 -1
- package/src/tools/atomic-file-write.ts +46 -15
- package/src/tools/auto-generated-guard.ts +3 -0
- package/src/tools/bash-allowed-prefixes.ts +153 -5
- package/src/tools/bash-interactive.ts +14 -2
- package/src/tools/bash.ts +14 -0
- package/src/tools/debug.ts +7 -6
- package/src/tools/descriptors.test.ts +44 -0
- package/src/tools/descriptors.ts +34 -2
- package/src/tools/fetch.ts +3 -2
- package/src/tools/index.ts +2 -0
- package/src/tools/output-meta.ts +23 -2
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +17 -35
- package/src/tools/report-issue.ts +184 -0
- package/src/tools/subagent-render.ts +114 -12
- package/src/tools/subagent.ts +108 -1
- package/src/tools/todo-write.ts +41 -3
- package/src/tools/tool-catalog.generated.ts +31 -3
- package/src/tools/write.ts +35 -3
- package/src/utils/commit-message-generator.ts +9 -2
- package/src/utils/file-mentions.ts +29 -1
- package/src/utils/title-generator.ts +7 -2
- package/src/web/scrapers/index.ts +3 -1
- package/src/web/scrapers/types.ts +8 -0
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/providers/insane.ts +91 -28
- package/src/web/search/providers/searxng.ts +62 -10
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import * as childProcess from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as process from "node:process";
|
|
5
|
+
import type * as stream from "node:stream";
|
|
6
|
+
import type { Process } from "@gajae-code/natives";
|
|
7
|
+
import { nativeProcessBindings } from "@gajae-code/utils/native-process";
|
|
8
|
+
import type { BrokerHopMessage } from "./ensure";
|
|
9
|
+
|
|
10
|
+
export const BROKER_HANDOFF_ACKNOWLEDGEMENT = "GJC_BROKER_HANDOFF_ACK\n";
|
|
11
|
+
export const BROKER_HANDOFF_COMMIT = "GJC_BROKER_HANDOFF_COMMIT\n";
|
|
12
|
+
export const BROKER_HANDOFF_ABORT = "GJC_BROKER_HANDOFF_ABORT\n";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Windows broker hop: spawns the real broker with detached:true and reports its pid.
|
|
16
|
+
*
|
|
17
|
+
* The hop is needed because a broker spawned with detached:true IS still killed when the
|
|
18
|
+
* parent process tree is terminated (e.g., by taskkill /T /F on Windows). Using an
|
|
19
|
+
* intermediate hop process that exits after spawning the real broker allows the broker
|
|
20
|
+
* to survive, because the hop's parent (the initiating client) can be killed without
|
|
21
|
+
* affecting the broker (the hop's pid is no longer valid).
|
|
22
|
+
*
|
|
23
|
+
* Invoked by gjc internals only. Usage:
|
|
24
|
+
* gjc internal broker-hop <json-encoded-args>
|
|
25
|
+
*
|
|
26
|
+
* Exits with code 0 after writing the broker pid/incarnation to stdout and
|
|
27
|
+
* receiving the parent's acknowledgement. If stdin closes first, it terminates
|
|
28
|
+
* the pinned broker and exits with code 1.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export async function runBrokerHopFromArgv(argv: string[]): Promise<void> {
|
|
32
|
+
if (argv.length !== 1) fail("broker hop requires exactly one argument");
|
|
33
|
+
let message: BrokerHopMessage;
|
|
34
|
+
try {
|
|
35
|
+
message = JSON.parse(argv[0]) as BrokerHopMessage;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
fail(`broker hop JSON parse error: ${error instanceof Error ? error.message : String(error)}`);
|
|
38
|
+
}
|
|
39
|
+
if (!message.command?.file || !Array.isArray(message.command.args)) fail("broker hop message missing command");
|
|
40
|
+
try {
|
|
41
|
+
// stderr is opened here by path: a parent fd number is meaningless in this process.
|
|
42
|
+
const stderr = message.stderrLogPath ? fs.openSync(message.stderrLogPath, "a") : "ignore";
|
|
43
|
+
// The broker inherits this process's environment, which the parent set to the
|
|
44
|
+
// broker environment; it is never carried on the command line.
|
|
45
|
+
const child = childProcess.spawn(message.command.file, message.command.args, {
|
|
46
|
+
detached: true,
|
|
47
|
+
windowsHide: true,
|
|
48
|
+
stdio: ["ignore", "ignore", stderr],
|
|
49
|
+
env: process.env,
|
|
50
|
+
...(message.cwd ? { cwd: message.cwd } : {}),
|
|
51
|
+
});
|
|
52
|
+
if (typeof stderr === "number") fs.closeSync(stderr);
|
|
53
|
+
const spawned = Promise.withResolvers<void>();
|
|
54
|
+
child.once("spawn", spawned.resolve);
|
|
55
|
+
child.once("error", spawned.reject);
|
|
56
|
+
await spawned.promise;
|
|
57
|
+
const pid = child.pid;
|
|
58
|
+
if (pid === undefined) fail("broker hop spawn succeeded but child pid unavailable");
|
|
59
|
+
const childReference = getSpawnedChildReference(pid);
|
|
60
|
+
if (!childReference) {
|
|
61
|
+
const cleanup = await terminateUnverifiableWindowsChild(child);
|
|
62
|
+
fail(`broker process identity could not be bound to the spawned child; ${cleanup}`);
|
|
63
|
+
}
|
|
64
|
+
const handoff = await writeBrokerHopReply(child, childReference, pid, writeBrokerHopStdout, () =>
|
|
65
|
+
waitForBrokerHandoffAcknowledgement(process.stdin),
|
|
66
|
+
);
|
|
67
|
+
if (handoff.kind === "failed") fail(handoff.reason);
|
|
68
|
+
process.exit(0);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
fail(`broker hop spawn failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type BrokerHopWriteCallback = (error?: Error | null) => void;
|
|
75
|
+
type BrokerHopWriter = (chunk: string, callback: BrokerHopWriteCallback) => void;
|
|
76
|
+
type BrokerHopAcknowledger = () => Promise<boolean>;
|
|
77
|
+
|
|
78
|
+
async function writeBrokerHopReply(
|
|
79
|
+
child: childProcess.ChildProcess,
|
|
80
|
+
reference: Process,
|
|
81
|
+
pid: number,
|
|
82
|
+
write: BrokerHopWriter,
|
|
83
|
+
waitForAcknowledgement: BrokerHopAcknowledger,
|
|
84
|
+
): Promise<{ kind: "written" } | { kind: "failed"; reason: string }> {
|
|
85
|
+
const completion = Promise.withResolvers<void>();
|
|
86
|
+
const acknowledgementPromise = waitForAcknowledgement();
|
|
87
|
+
let settled = false;
|
|
88
|
+
const settle = (error?: Error | null): void => {
|
|
89
|
+
if (settled) return;
|
|
90
|
+
settled = true;
|
|
91
|
+
if (error) completion.reject(error);
|
|
92
|
+
else completion.resolve();
|
|
93
|
+
};
|
|
94
|
+
try {
|
|
95
|
+
write(`${JSON.stringify({ pid, incarnation: reference.incarnation })}\n`, settle);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
settle(error instanceof Error ? error : new Error(String(error)));
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await completion.promise;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const writeError = error instanceof Error ? error : new Error(String(error));
|
|
103
|
+
const cleanup = await terminateBrokerAfterFailedHandoff(reference);
|
|
104
|
+
return {
|
|
105
|
+
kind: "failed",
|
|
106
|
+
reason: `broker hop handoff failed for pid ${pid}: ${cleanup}; ${writeError.message}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
let acknowledged = false;
|
|
110
|
+
let acknowledgementError: string | undefined;
|
|
111
|
+
try {
|
|
112
|
+
acknowledged = await acknowledgementPromise;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
acknowledgementError = error instanceof Error ? error.message : String(error);
|
|
115
|
+
}
|
|
116
|
+
if (!acknowledged) {
|
|
117
|
+
const cleanup = await terminateBrokerAfterFailedHandoff(reference);
|
|
118
|
+
return {
|
|
119
|
+
kind: "failed",
|
|
120
|
+
reason: `broker hop handoff was not acknowledged for pid ${pid}: ${cleanup}${acknowledgementError ? `; ${acknowledgementError}` : ""}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
child.unref();
|
|
124
|
+
return { kind: "written" };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function writeBrokerHopReplyForTest(
|
|
128
|
+
child: childProcess.ChildProcess,
|
|
129
|
+
reference: Process,
|
|
130
|
+
pid: number,
|
|
131
|
+
write: BrokerHopWriter,
|
|
132
|
+
waitForAcknowledgement: BrokerHopAcknowledger = async () => true,
|
|
133
|
+
): Promise<{ kind: "written" } | { kind: "failed"; reason: string }> {
|
|
134
|
+
return writeBrokerHopReply(child, reference, pid, write, waitForAcknowledgement);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function waitForBrokerHandoffAcknowledgement(input: stream.Readable): Promise<boolean> {
|
|
138
|
+
const acknowledgment = Promise.withResolvers<boolean>();
|
|
139
|
+
let received = "";
|
|
140
|
+
let settled = false;
|
|
141
|
+
const finish = (accepted: boolean): void => {
|
|
142
|
+
if (settled) return;
|
|
143
|
+
settled = true;
|
|
144
|
+
input.removeListener("data", onData);
|
|
145
|
+
input.removeListener("end", onEnd);
|
|
146
|
+
input.removeListener("close", onEnd);
|
|
147
|
+
input.removeListener("error", onEnd);
|
|
148
|
+
acknowledgment.resolve(accepted);
|
|
149
|
+
};
|
|
150
|
+
const onData = (chunk: Buffer | string): void => {
|
|
151
|
+
received += chunk.toString();
|
|
152
|
+
if (received.length > BROKER_HANDOFF_ACKNOWLEDGEMENT.length) {
|
|
153
|
+
finish(false);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (received.includes("\n")) finish(received === BROKER_HANDOFF_ACKNOWLEDGEMENT);
|
|
157
|
+
};
|
|
158
|
+
const onEnd = (): void => finish(false);
|
|
159
|
+
input.on("data", onData);
|
|
160
|
+
input.once("end", onEnd);
|
|
161
|
+
input.once("close", onEnd);
|
|
162
|
+
input.once("error", onEnd);
|
|
163
|
+
input.resume();
|
|
164
|
+
return acknowledgment.promise;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Wait for a validated identity ACK followed by the parent's publication decision. */
|
|
168
|
+
export function waitForBrokerHandoffDecision(input: stream.Readable, timeoutMs = 30_000): Promise<"commit" | "abort"> {
|
|
169
|
+
const decision = Promise.withResolvers<"commit" | "abort">();
|
|
170
|
+
let received = "";
|
|
171
|
+
let settled = false;
|
|
172
|
+
const finish = (value: "commit" | "abort"): void => {
|
|
173
|
+
if (settled) return;
|
|
174
|
+
settled = true;
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
input.removeListener("data", onData);
|
|
177
|
+
input.removeListener("end", onEnd);
|
|
178
|
+
input.removeListener("close", onEnd);
|
|
179
|
+
input.removeListener("error", onEnd);
|
|
180
|
+
decision.resolve(value);
|
|
181
|
+
};
|
|
182
|
+
const onData = (chunk: Buffer | string): void => {
|
|
183
|
+
received += chunk.toString();
|
|
184
|
+
if (received.length > BROKER_HANDOFF_ACKNOWLEDGEMENT.length + BROKER_HANDOFF_COMMIT.length) {
|
|
185
|
+
finish("abort");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (received.length < BROKER_HANDOFF_ACKNOWLEDGEMENT.length) {
|
|
189
|
+
if (!BROKER_HANDOFF_ACKNOWLEDGEMENT.startsWith(received)) finish("abort");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!received.startsWith(BROKER_HANDOFF_ACKNOWLEDGEMENT)) {
|
|
193
|
+
finish("abort");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const action = received.slice(BROKER_HANDOFF_ACKNOWLEDGEMENT.length);
|
|
197
|
+
if (action === BROKER_HANDOFF_COMMIT) finish("commit");
|
|
198
|
+
else if (action === BROKER_HANDOFF_ABORT) finish("abort");
|
|
199
|
+
else if (!BROKER_HANDOFF_COMMIT.startsWith(action) && !BROKER_HANDOFF_ABORT.startsWith(action)) finish("abort");
|
|
200
|
+
};
|
|
201
|
+
const onEnd = (): void => finish("abort");
|
|
202
|
+
const timer = setTimeout(() => finish("abort"), timeoutMs);
|
|
203
|
+
input.on("data", onData);
|
|
204
|
+
input.once("end", onEnd);
|
|
205
|
+
input.once("close", onEnd);
|
|
206
|
+
input.once("error", onEnd);
|
|
207
|
+
input.resume();
|
|
208
|
+
return decision.promise;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const writeBrokerHopStdout: BrokerHopWriter = (chunk, callback): void => {
|
|
212
|
+
const onError = (error: Error): void => {
|
|
213
|
+
process.stdout.removeListener("error", onError);
|
|
214
|
+
callback(error);
|
|
215
|
+
};
|
|
216
|
+
process.stdout.once("error", onError);
|
|
217
|
+
try {
|
|
218
|
+
process.stdout.write(chunk, error => {
|
|
219
|
+
process.stdout.removeListener("error", onError);
|
|
220
|
+
callback(error);
|
|
221
|
+
});
|
|
222
|
+
} catch (error) {
|
|
223
|
+
process.stdout.removeListener("error", onError);
|
|
224
|
+
callback(error instanceof Error ? error : new Error(String(error)));
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/** Pin only the still-running direct child so a reused PID cannot be adopted. */
|
|
229
|
+
function getSpawnedChildReference(pid: number): Process | undefined {
|
|
230
|
+
try {
|
|
231
|
+
const reference = nativeProcessBindings().Process.fromPid(pid);
|
|
232
|
+
if (!reference || reference.pid !== pid || reference.ppid !== process.pid || reference.status() !== "running")
|
|
233
|
+
return undefined;
|
|
234
|
+
return reference;
|
|
235
|
+
} catch {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function terminateUnverifiableWindowsChild(child: childProcess.ChildProcess): Promise<string> {
|
|
241
|
+
if (process.platform !== "win32") return "no signal was sent without a verified process identity";
|
|
242
|
+
if (child.exitCode !== null || child.signalCode !== null) return "the spawned child already exited";
|
|
243
|
+
const exited = Promise.withResolvers<void>();
|
|
244
|
+
child.once("exit", exited.resolve);
|
|
245
|
+
child.once("close", exited.resolve);
|
|
246
|
+
child.on("error", () => {});
|
|
247
|
+
try {
|
|
248
|
+
child.kill("SIGKILL");
|
|
249
|
+
} catch {
|
|
250
|
+
// The process handle remains exact; inspect its exit state below.
|
|
251
|
+
}
|
|
252
|
+
await Promise.race([exited.promise, Bun.sleep(500)]);
|
|
253
|
+
return child.exitCode !== null || child.signalCode !== null
|
|
254
|
+
? "the exact spawned child was terminated"
|
|
255
|
+
: "termination of the exact spawned child was not confirmed";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function terminateBrokerAfterFailedHandoff(reference: Process): Promise<string> {
|
|
259
|
+
let cleanup: string;
|
|
260
|
+
try {
|
|
261
|
+
if (reference.status() === "running") reference.signalRoot(os.constants.signals.SIGKILL);
|
|
262
|
+
const exited = await reference.waitForExit({ timeoutMs: 500 });
|
|
263
|
+
cleanup =
|
|
264
|
+
exited || reference.status() !== "running" ? "broker terminated" : "broker termination was not confirmed";
|
|
265
|
+
} catch (error) {
|
|
266
|
+
cleanup = `broker termination failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
267
|
+
}
|
|
268
|
+
return cleanup;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function fail(message: string): never {
|
|
272
|
+
process.stderr.write(`gjc: ${message}\n`);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
@@ -130,6 +130,10 @@ type BoundedSourceRead =
|
|
|
130
130
|
|
|
131
131
|
const terminal = (s: LifecycleState) => s === "terminal_ok" || s === "terminal_error";
|
|
132
132
|
const final = (s: LifecycleState) => terminal(s) || s === "terminal_uncertain";
|
|
133
|
+
const recordedSessionIds = (entry: LifecycleLedgerEntry): string[] =>
|
|
134
|
+
[entry.intendedSessionId, entry.resultSessionId].filter(
|
|
135
|
+
(sessionId): sessionId is string => typeof sessionId === "string" && sessionId.length > 0,
|
|
136
|
+
);
|
|
133
137
|
export interface LifecycleLedgerLimits {
|
|
134
138
|
maxBytes?: number;
|
|
135
139
|
maxLineBytes?: number;
|
|
@@ -795,27 +799,47 @@ export class LifecycleLedger {
|
|
|
795
799
|
return [...this.#byIdentity.values()].findLast(entry => entry.operationKey === operationKey);
|
|
796
800
|
}
|
|
797
801
|
/**
|
|
798
|
-
* Live
|
|
799
|
-
*
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
*
|
|
802
|
+
* Live legacy rows predate the operation/key index. Their opaque identities
|
|
803
|
+
* cannot establish that a different target is safe, so callers must reject
|
|
804
|
+
* rather than create a second admission. A fresh session.create may ignore
|
|
805
|
+
* final legacy rows: exact legacy identities are still resolved before this
|
|
806
|
+
* check, and uncertain rows retain their session-scoped fences.
|
|
807
|
+
*
|
|
808
|
+
* A session-targeted request passes `sessionTarget` to ignore terminal legacy
|
|
809
|
+
* rows that record only other sessions, unless the row is the caller's key
|
|
810
|
+
* reused across targets (`keyReuseIdentities`). Terminal rows recording the
|
|
811
|
+
* target or no session, uncertain rows, and live rows keep the fence because
|
|
812
|
+
* they cannot prove the requested target is unrelated.
|
|
807
813
|
*/
|
|
808
814
|
hasLegacyIdentity(
|
|
809
815
|
excludedIdentities?: ReadonlySet<string>,
|
|
810
|
-
options: {
|
|
816
|
+
options: {
|
|
817
|
+
ignoreFinalRows?: boolean;
|
|
818
|
+
sessionTarget?: { sessionId: string; keyReuseIdentities: ReadonlySet<string> };
|
|
819
|
+
} = {},
|
|
811
820
|
): boolean {
|
|
821
|
+
const target = options.sessionTarget;
|
|
822
|
+
const unrelatedTerminal = (entry: LifecycleLedgerEntry): boolean => {
|
|
823
|
+
if (!target || !terminal(entry.state) || target.keyReuseIdentities.has(entry.identity)) return false;
|
|
824
|
+
const sessionIds = recordedSessionIds(entry);
|
|
825
|
+
return sessionIds.length > 0 && !sessionIds.includes(target.sessionId);
|
|
826
|
+
};
|
|
812
827
|
return [...this.#byIdentity.values()].some(
|
|
813
828
|
entry =>
|
|
814
829
|
entry.operationKey === undefined &&
|
|
815
|
-
!(options.
|
|
830
|
+
!(options.ignoreFinalRows && final(entry.state)) &&
|
|
831
|
+
!unrelatedTerminal(entry) &&
|
|
816
832
|
!excludedIdentities?.has(entry.identity),
|
|
817
833
|
);
|
|
818
834
|
}
|
|
835
|
+
/** Sessions recorded by terminal legacy rows, for deriving key-reuse identities. */
|
|
836
|
+
terminalLegacySessionIds(): Set<string> {
|
|
837
|
+
const sessionIds = new Set<string>();
|
|
838
|
+
for (const entry of this.#byIdentity.values())
|
|
839
|
+
if (entry.operationKey === undefined && terminal(entry.state))
|
|
840
|
+
for (const sessionId of recordedSessionIds(entry)) sessionIds.add(sessionId);
|
|
841
|
+
return sessionIds;
|
|
842
|
+
}
|
|
819
843
|
async migrateIdentity(
|
|
820
844
|
from: string,
|
|
821
845
|
to: string,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { loadInstallationHostId, loadLegacyInstallationHostId } from "../../config/machine-identity";
|
|
3
|
+
|
|
4
|
+
export interface BrokerLockHostIdentity {
|
|
5
|
+
ownerHostId: string;
|
|
6
|
+
previousOwnerHostIds: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const brokerLockHostIdentityPromises = new Map<string, Promise<BrokerLockHostIdentity>>();
|
|
10
|
+
|
|
11
|
+
export async function brokerLockHostIdentity(agentDir: string): Promise<BrokerLockHostIdentity> {
|
|
12
|
+
const coordinationRoot = path.resolve(agentDir, "sdk");
|
|
13
|
+
const pending =
|
|
14
|
+
brokerLockHostIdentityPromises.get(coordinationRoot) ??
|
|
15
|
+
Promise.all([loadInstallationHostId({ configRootDir: coordinationRoot }), loadLegacyInstallationHostId()]).then(
|
|
16
|
+
([ownerHostId, legacyHostId]) => ({
|
|
17
|
+
ownerHostId,
|
|
18
|
+
previousOwnerHostIds: legacyHostId === ownerHostId ? [] : [legacyHostId],
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
brokerLockHostIdentityPromises.set(coordinationRoot, pending);
|
|
22
|
+
try {
|
|
23
|
+
return await pending;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (brokerLockHostIdentityPromises.get(coordinationRoot) === pending)
|
|
26
|
+
brokerLockHostIdentityPromises.delete(coordinationRoot);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -3,7 +3,12 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { logger, resolveEquivalentPath } from "@gajae-code/utils";
|
|
5
5
|
import { nativeProcessBindings } from "@gajae-code/utils/native-process";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
FILE_LOCK_ORPHAN_TRANSITION_ADOPTION_AGE_MS,
|
|
8
|
+
type FileLockOptions,
|
|
9
|
+
isFileLockAcquireTimeout,
|
|
10
|
+
withFileLock,
|
|
11
|
+
} from "../../config/file-lock";
|
|
7
12
|
import { repo } from "../../utils/git";
|
|
8
13
|
import { endpointIncarnation } from "./endpoint-authority";
|
|
9
14
|
import { processIncarnation } from "./process-incarnation";
|
|
@@ -574,6 +579,7 @@ function projectIdentity(
|
|
|
574
579
|
ambiguous: boolean,
|
|
575
580
|
now: number,
|
|
576
581
|
probedIncarnations?: ReadonlyMap<string, string | undefined>,
|
|
582
|
+
skipOsProcessProbes = false,
|
|
577
583
|
): IndexedSession {
|
|
578
584
|
const { latest, heartbeat } = state;
|
|
579
585
|
const terminal = isTerminalEvent(latest);
|
|
@@ -582,7 +588,7 @@ function projectIdentity(
|
|
|
582
588
|
latest.hostUnregisteredReason === "process_exited" || latest.hostUnregisteredReason === "detached_idle"
|
|
583
589
|
? latest.hostUnregisteredReason
|
|
584
590
|
: undefined;
|
|
585
|
-
const pidAlive = alive(latest.pid);
|
|
591
|
+
const pidAlive = !skipOsProcessProbes && alive(latest.pid);
|
|
586
592
|
// Liveness evidence is host-written: a checkpointed heartbeat, or the
|
|
587
593
|
// `host_registered` event the host appended itself. Counting registration
|
|
588
594
|
// closes the up-to-one-interval window where a just-registered session
|
|
@@ -602,7 +608,7 @@ function projectIdentity(
|
|
|
602
608
|
// costing a powershell.exe spawn per stale registration on Windows.
|
|
603
609
|
const probeKey = `${latest.sessionId}\u0000${latest.endpointGeneration}\u0000${latest.pid}`;
|
|
604
610
|
const currentIncarnation =
|
|
605
|
-
recordedIncarnation === undefined || !pidAlive
|
|
611
|
+
skipOsProcessProbes || recordedIncarnation === undefined || !pidAlive
|
|
606
612
|
? undefined
|
|
607
613
|
: probedIncarnations !== undefined
|
|
608
614
|
? probedIncarnations.get(probeKey)
|
|
@@ -654,7 +660,9 @@ function reduceEvents(
|
|
|
654
660
|
agentDir: string,
|
|
655
661
|
probedIncarnations?: ReadonlyMap<string, string | undefined>,
|
|
656
662
|
sessionIds?: ReadonlySet<string>,
|
|
663
|
+
options: { readonly skipOsProcessProbes?: boolean } = {},
|
|
657
664
|
): SessionIndexProjection {
|
|
665
|
+
const skipOsProcessProbes = options.skipOsProcessProbes === true;
|
|
658
666
|
// Resolved once: the fence-row check runs per root, and only this side of the
|
|
659
667
|
// comparison is ours to normalize.
|
|
660
668
|
const agentDirIdentity = resolveEquivalentPath(agentDir);
|
|
@@ -741,7 +749,8 @@ function reduceEvents(
|
|
|
741
749
|
const fencing = fencingRoots.get(sessionId);
|
|
742
750
|
const unresolved = unresolvedRoots.get(sessionId);
|
|
743
751
|
const ambiguous = (fencing?.size ?? 0) > 1;
|
|
744
|
-
for (const state of states)
|
|
752
|
+
for (const state of states)
|
|
753
|
+
identities.push(projectIdentity(state, ambiguous, now, probedIncarnations, skipOsProcessProbes));
|
|
745
754
|
const defaultAuthority = preferredIdentity(states);
|
|
746
755
|
if (defaultAuthority === undefined || defaultAuthority.latest.type === "session_deleted") continue;
|
|
747
756
|
let authority = defaultAuthority;
|
|
@@ -754,14 +763,42 @@ function reduceEvents(
|
|
|
754
763
|
const survivingAuthority = onlyRoot ? preferredIdentity(onlyRoot) : undefined;
|
|
755
764
|
if (survivingAuthority !== undefined) authority = survivingAuthority;
|
|
756
765
|
}
|
|
757
|
-
sessions.push(projectIdentity(authority, ambiguous, now, probedIncarnations));
|
|
766
|
+
sessions.push(projectIdentity(authority, ambiguous, now, probedIncarnations, skipOsProcessProbes));
|
|
758
767
|
}
|
|
759
768
|
return { identities, sessions };
|
|
760
769
|
}
|
|
761
770
|
|
|
762
771
|
// Global launch bursts may queue behind legitimate long index transactions. Keep
|
|
763
772
|
// this bounded at one minute while the shared lock's exact dead-owner recovery runs.
|
|
764
|
-
const SESSION_INDEX_LOCK_OPTIONS = {
|
|
773
|
+
const SESSION_INDEX_LOCK_OPTIONS = {
|
|
774
|
+
retries: 600,
|
|
775
|
+
retryDelayMs: 100,
|
|
776
|
+
orphanTransitionAgeMs: FILE_LOCK_ORPHAN_TRANSITION_ADOPTION_AGE_MS,
|
|
777
|
+
} as const;
|
|
778
|
+
// Leave one retry interval beyond the transition's safe-adoption age so a fresh
|
|
779
|
+
// ownerless transition can be reclaimed during ordinary startup.
|
|
780
|
+
const SESSION_INDEX_STARTUP_LOCK_WAIT_MS =
|
|
781
|
+
FILE_LOCK_ORPHAN_TRANSITION_ADOPTION_AGE_MS + SESSION_INDEX_LOCK_OPTIONS.retryDelayMs * 2;
|
|
782
|
+
|
|
783
|
+
/** Bound ordinary bootstrap waits; authorized doctor successors may use the full prepared window. */
|
|
784
|
+
export function sessionIndexStartupLockOptions(
|
|
785
|
+
deadline: number,
|
|
786
|
+
signal?: AbortSignal,
|
|
787
|
+
policy: "ordinary" | "authorized-successor" = "ordinary",
|
|
788
|
+
): FileLockOptions {
|
|
789
|
+
const boundedDeadline =
|
|
790
|
+
policy === "authorized-successor"
|
|
791
|
+
? deadline
|
|
792
|
+
: Math.min(deadline, performance.now() + SESSION_INDEX_STARTUP_LOCK_WAIT_MS);
|
|
793
|
+
return {
|
|
794
|
+
...SESSION_INDEX_LOCK_OPTIONS,
|
|
795
|
+
retries: Math.min(
|
|
796
|
+
SESSION_INDEX_LOCK_OPTIONS.retries,
|
|
797
|
+
Math.max(1, Math.ceil((boundedDeadline - performance.now()) / SESSION_INDEX_LOCK_OPTIONS.retryDelayMs)),
|
|
798
|
+
),
|
|
799
|
+
...(signal === undefined ? {} : { signal }),
|
|
800
|
+
};
|
|
801
|
+
}
|
|
765
802
|
|
|
766
803
|
/**
|
|
767
804
|
* Observation window for one locked index transaction (#4544). This never times
|
|
@@ -1183,7 +1220,7 @@ export class SessionIndex {
|
|
|
1183
1220
|
});
|
|
1184
1221
|
return promise;
|
|
1185
1222
|
}
|
|
1186
|
-
async open(): Promise<this> {
|
|
1223
|
+
async open(options: FileLockOptions = SESSION_INDEX_LOCK_OPTIONS): Promise<this> {
|
|
1187
1224
|
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
1188
1225
|
let group = SessionIndex.#openGroups.get(indexPath);
|
|
1189
1226
|
if (!group || group.closed) {
|
|
@@ -1193,7 +1230,7 @@ export class SessionIndex {
|
|
|
1193
1230
|
}
|
|
1194
1231
|
await group.promise;
|
|
1195
1232
|
await SessionIndex.#enqueue(indexPath, () =>
|
|
1196
|
-
withSessionIndexLock("replay", this.#agentDir, () => this.#replayUnderLock()),
|
|
1233
|
+
withSessionIndexLock("replay", this.#agentDir, () => this.#replayUnderLock(), options),
|
|
1197
1234
|
);
|
|
1198
1235
|
return this;
|
|
1199
1236
|
}
|
|
@@ -2455,14 +2492,25 @@ export class SessionIndex {
|
|
|
2455
2492
|
// revalidation that never probes the OS under the lock: the next pass
|
|
2456
2493
|
// re-probes from scratch.
|
|
2457
2494
|
const probed = new Map<string, string | undefined>();
|
|
2458
|
-
|
|
2495
|
+
// This projection is only used to select structurally eligible rows; its
|
|
2496
|
+
// `live` field is not consumed here. Skip OS probes during projection so
|
|
2497
|
+
// the deadline-aware loop below performs only the required observations.
|
|
2498
|
+
const candidates = reduceEvents(this.#events, now, this.#agentDir, undefined, undefined, {
|
|
2499
|
+
skipOsProcessProbes: true,
|
|
2500
|
+
}).sessions;
|
|
2501
|
+
if (signal.aborted || performance.now() >= deadline) return 0;
|
|
2502
|
+
for (const row of candidates) {
|
|
2503
|
+
if (signal.aborted || performance.now() >= deadline) return 0;
|
|
2459
2504
|
if (!isSessionAuthorityEligible(row) || row.terminal || row.terminalUncertain) continue;
|
|
2460
2505
|
if (row.lastHeartbeatAt !== undefined && now - row.lastHeartbeatAt < SESSION_HEARTBEAT_INTERVAL_MS)
|
|
2461
2506
|
continue;
|
|
2462
|
-
if (!alive(row.pid)) continue;
|
|
2463
2507
|
const recordedIncarnation = row.hostIncarnation ?? row.processIncarnation;
|
|
2464
2508
|
if (recordedIncarnation === undefined) continue;
|
|
2465
|
-
|
|
2509
|
+
if (!alive(row.pid)) continue;
|
|
2510
|
+
if (signal.aborted || performance.now() >= deadline) return 0;
|
|
2511
|
+
const currentIncarnation = processIncarnation(row.pid);
|
|
2512
|
+
if (signal.aborted || performance.now() >= deadline) return 0;
|
|
2513
|
+
probed.set(`${row.sessionId}\u0000${row.endpointGeneration}\u0000${row.pid}`, currentIncarnation);
|
|
2466
2514
|
}
|
|
2467
2515
|
const probedAt = performance.now();
|
|
2468
2516
|
if (signal.aborted || probedAt >= deadline) return 0;
|
|
@@ -2492,7 +2540,9 @@ export class SessionIndex {
|
|
|
2492
2540
|
if (performance.now() - probedAt > SESSION_INDEX_REPLAY_FRESHNESS_MS) return STALE_OBSERVATION;
|
|
2493
2541
|
if (this.#corruptSuffix) return 0;
|
|
2494
2542
|
const events: SessionIndexEvent[] = [];
|
|
2495
|
-
const rows = reduceEvents(this.#events, now, this.#agentDir, probed
|
|
2543
|
+
const rows = reduceEvents(this.#events, now, this.#agentDir, probed, undefined, {
|
|
2544
|
+
skipOsProcessProbes: true,
|
|
2545
|
+
}).sessions;
|
|
2496
2546
|
for (const row of rows) {
|
|
2497
2547
|
if (!isSessionAuthorityEligible(row) || row.terminal || row.terminalUncertain) continue;
|
|
2498
2548
|
if (row.lastHeartbeatAt !== undefined && now - row.lastHeartbeatAt < SESSION_HEARTBEAT_INTERVAL_MS)
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AdapterValidationError,
|
|
3
|
+
validateAdapterControl,
|
|
4
|
+
validateAdapterSecretFields,
|
|
5
|
+
} from "../protocol/adapter-validation";
|
|
2
6
|
import { type Adapter, OPERATIONS, type Operation, type OperationKind } from "../protocol/operation-registry";
|
|
3
7
|
|
|
4
8
|
export type ChatTransport = Extract<Adapter, "telegram" | "discord" | "slack">;
|
|
5
9
|
export type ChatOperationDisposition = "allowed" | "unsupported_on_chat";
|
|
6
10
|
|
|
7
11
|
export interface ChatCommandError {
|
|
8
|
-
code: "unsupported_on_chat" | "secret_input_forbidden";
|
|
12
|
+
code: "unsupported_on_chat" | "secret_input_forbidden" | "ultrafast_tier_forbidden" | AdapterValidationError["code"];
|
|
9
13
|
message: string;
|
|
10
14
|
}
|
|
11
15
|
|
|
@@ -57,12 +61,21 @@ export function authorizeChatOperation(transport: ChatTransport, request: ChatOp
|
|
|
57
61
|
candidate => candidate.kind === request.kind && candidate.sdkId === request.operation,
|
|
58
62
|
);
|
|
59
63
|
if (!operation) return unsupported();
|
|
60
|
-
|
|
64
|
+
const secretValidation = validateAdapterSecretFields(
|
|
65
|
+
operation.sdkId,
|
|
66
|
+
(request.input ?? {}) as Record<string, unknown>,
|
|
67
|
+
);
|
|
68
|
+
if (secretValidation?.code === "secret_field_forbidden") {
|
|
61
69
|
return {
|
|
62
70
|
ok: false,
|
|
63
71
|
error: { code: "secret_input_forbidden", message: "Secret configuration input is forbidden on chat." },
|
|
64
72
|
};
|
|
65
73
|
}
|
|
74
|
+
if (secretValidation) return { ok: false, error: secretValidation };
|
|
75
|
+
if (operation.kind === "control") {
|
|
76
|
+
const invalid = validateAdapterControl(operation.sdkId, (request.input ?? {}) as Record<string, unknown>);
|
|
77
|
+
if (invalid) return { ok: false, error: invalid };
|
|
78
|
+
}
|
|
66
79
|
return CHAT_OPERATION_POLICY[transport][operation.id] === "allowed" ? { ok: true } : unsupported();
|
|
67
80
|
}
|
|
68
81
|
|