@gajae-code/coding-agent 0.12.11 → 0.12.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 +38 -0
- package/dist/types/cli/notify-cli.d.ts +42 -1
- package/dist/types/cli/update-cli.d.ts +9 -3
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/main.d.ts +6 -2
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +23 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
- package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
- package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
- package/dist/types/sdk/bus/notification-service.d.ts +10 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +24 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +104 -16
- package/dist/types/sdk/bus/telegram-reference.d.ts +10 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
- package/dist/types/sdk/host/host.d.ts +52 -0
- package/dist/types/sdk/lifecycle-session.d.ts +6 -0
- package/dist/types/sdk/session-activation.d.ts +41 -0
- package/dist/types/sdk/session.d.ts +18 -0
- package/dist/types/sdk/startup-capability.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +7 -0
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +23 -5
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/utils/npm-registry.d.ts +71 -0
- package/examples/extensions/README.md +38 -4
- package/examples/extensions/ooo-bridge.ts +15 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/src/async/job-manager.ts +9 -2
- package/src/cli/notify-cli.ts +226 -2
- package/src/cli/update-cli.ts +90 -23
- package/src/commands/notify.ts +27 -3
- package/src/commands/sdk.ts +1 -0
- package/src/config/file-lock-gc.ts +18 -0
- package/src/config/file-lock.ts +61 -16
- package/src/config/model-profiles.ts +31 -15
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +2 -0
- package/src/coordinator-mcp/server.ts +358 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
- package/src/extensibility/extensions/runner.ts +15 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/launch-tmux.ts +53 -1
- package/src/internal-urls/docs-index.generated.ts +11 -10
- package/src/main.ts +203 -67
- package/src/modes/DESIGN.md +8 -5
- package/src/modes/components/footer.ts +10 -18
- package/src/modes/components/session-selector.ts +26 -3
- package/src/modes/components/status-line/gh.ts +23 -2
- package/src/modes/controllers/event-controller.ts +3 -0
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +11 -0
- package/src/modes/controllers/selector-controller.ts +8 -2
- package/src/modes/interactive-mode.ts +7 -1
- package/src/modes/utils/host-status.ts +38 -0
- package/src/runtime-mcp/types.ts +2 -0
- package/src/sdk/broker/lifecycle.ts +60 -3
- package/src/sdk/bus/chat-daemon-cli.ts +42 -0
- package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
- package/src/sdk/bus/chat-daemon-control.ts +16 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
- package/src/sdk/bus/control-plane-frames.ts +51 -0
- package/src/sdk/bus/conversation-store.ts +29 -1
- package/src/sdk/bus/existing-thread-readiness.ts +155 -0
- package/src/sdk/bus/index.ts +70 -0
- package/src/sdk/bus/notification-service.ts +28 -1
- package/src/sdk/bus/rich-render.ts +3 -2
- package/src/sdk/bus/slack-daemon.ts +99 -4
- package/src/sdk/bus/slack-live-provider.ts +32 -2
- package/src/sdk/bus/slack-provider.ts +11 -0
- package/src/sdk/bus/slack-thread-binding.ts +641 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +24 -7
- package/src/sdk/bus/telegram-daemon.ts +1961 -497
- package/src/sdk/bus/telegram-reference.ts +42 -6
- package/src/sdk/bus/topic-registry.ts +674 -181
- package/src/sdk/host/host.ts +180 -1
- package/src/sdk/lifecycle-session.ts +8 -2
- package/src/sdk/protocol/operation-inventory.generated.json +22 -0
- package/src/sdk/session-activation.ts +127 -0
- package/src/sdk/session.ts +139 -28
- package/src/sdk/startup-capability.ts +10 -1
- package/src/session/agent-session.ts +220 -9
- package/src/session/artifacts.ts +56 -13
- package/src/session/internal/managed-session-storage.ts +206 -7
- package/src/session/session-manager.ts +148 -38
- package/src/setup/provider-presets.json +1 -1
- package/src/task/index.ts +92 -44
- package/src/tools/index.ts +8 -0
- package/src/utils/npm-registry.ts +660 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export declare const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
|
|
2
|
+
export type NpmConfigOrigin = "env" | "user" | "global";
|
|
3
|
+
/** Which package manager will run the install this check is gating. */
|
|
4
|
+
export type Installer = "bun" | "npm";
|
|
5
|
+
/** Injection points so tests never read the real environment. */
|
|
6
|
+
export interface NpmRegistryEnvironment {
|
|
7
|
+
/** Manager that will perform the install; decides Bun-vs-npm config priority. */
|
|
8
|
+
installer?: Installer;
|
|
9
|
+
/** Resolves an executable on PATH; used to locate npm's effective prefix. */
|
|
10
|
+
whichExecutable?: (name: string) => string | undefined;
|
|
11
|
+
/** Defaults to `$credentialEnv`, which excludes the caller's `cwd/.env`. */
|
|
12
|
+
lookupEnv?: (name: string) => string | undefined;
|
|
13
|
+
homeDir?: string;
|
|
14
|
+
platform?: NodeJS.Platform;
|
|
15
|
+
readFile?: (filePath: string) => Promise<string | undefined>;
|
|
16
|
+
}
|
|
17
|
+
export interface ResolvedNpmRegistry {
|
|
18
|
+
registry: string;
|
|
19
|
+
headers: Record<string, string>;
|
|
20
|
+
origin: NpmConfigOrigin | "default";
|
|
21
|
+
/** Where the value came from, e.g. `$npm_config_registry` or `~/.npmrc`. */
|
|
22
|
+
source: string;
|
|
23
|
+
/** Config files that exist but could not be read. */
|
|
24
|
+
warnings: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface LatestPackageVersion {
|
|
27
|
+
version: string;
|
|
28
|
+
registry: string;
|
|
29
|
+
/** Config problems that did not stop the lookup but changed its outcome. */
|
|
30
|
+
warnings: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface FetchResponseLike {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
status: number;
|
|
35
|
+
statusText: string;
|
|
36
|
+
json: () => Promise<unknown>;
|
|
37
|
+
}
|
|
38
|
+
export type FetchLike = (input: string, init?: {
|
|
39
|
+
headers?: Record<string, string>;
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
}) => Promise<FetchResponseLike>;
|
|
42
|
+
export interface NpmRegistryLookupOptions extends NpmRegistryEnvironment {
|
|
43
|
+
fetchImpl?: FetchLike;
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
}
|
|
46
|
+
/** Thrown when a registry is configured but unusable, so it is never silently ignored. */
|
|
47
|
+
export declare class NpmRegistryConfigError extends Error {
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse an `.npmrc` into raw key/value pairs. Later duplicates win, as in npm.
|
|
51
|
+
*
|
|
52
|
+
* @internal Exported for tests; production code goes through `resolveNpmRegistry`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseNpmrc(contents: string, lookupEnv?: (name: string) => string | undefined): Map<string, string>;
|
|
55
|
+
/**
|
|
56
|
+
* Build the credential lookup keys npm calls "nerf darts": `//host/path/`,
|
|
57
|
+
* walking up one path segment at a time until the bare host.
|
|
58
|
+
*
|
|
59
|
+
* @internal Exported for tests; production code goes through `resolveNpmRegistry`.
|
|
60
|
+
*/
|
|
61
|
+
export declare function credentialPrefixes(registry: string): string[];
|
|
62
|
+
/** Resolve the registry npm/bun would use for `packageName`, with credentials attached. */
|
|
63
|
+
export declare function resolveNpmRegistry(packageName: string, environment?: NpmRegistryEnvironment): Promise<ResolvedNpmRegistry>;
|
|
64
|
+
/**
|
|
65
|
+
* Registries can live under a path prefix, so join by string rather than `new URL`.
|
|
66
|
+
*
|
|
67
|
+
* @internal Exported for tests; production code goes through `fetchLatestPackageVersion`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function buildRegistryPackageUrl(registry: string, packageName: string, spec?: string): string;
|
|
70
|
+
/** Fetch the latest published version of `packageName` from the configured registry. */
|
|
71
|
+
export declare function fetchLatestPackageVersion(packageName: string, options?: NpmRegistryLookupOptions): Promise<LatestPackageVersion>;
|
|
@@ -14,6 +14,39 @@ mkdir -p .gjc/extensions
|
|
|
14
14
|
cp packages/coding-agent/examples/extensions/pirate.ts .gjc/extensions/
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
### Enable the Ouroboros `ooo` bridge
|
|
18
|
+
|
|
19
|
+
Install the version-pinned Ouroboros `v0.50.7` MCP profile, then configure its GJC runtime:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv tool install 'ouroboros-ai[mcp]==0.50.7'
|
|
23
|
+
ouroboros setup --runtime gjc
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`pipx install 'ouroboros-ai[mcp]==0.50.7'` is the equivalent pipx installation. Do not pipe a mutable branch installer into a shell. Pin source audits to commit `cb658aa819bfabafecbbe91bc36327f10691171b`. The [v0.50.7 release](https://github.com/Q00/ouroboros/releases/tag/v0.50.7) publishes `ouroboros_ai-0.50.7-py3-none-any.whl` with SHA-256 `df42f4ef10e032f2edc3249534bf91e8612dee789dfc3517895a9eb2df7f82c4`; verify downloaded release assets before installing them.
|
|
27
|
+
|
|
28
|
+
Ouroboros setup installs its own managed bridge. Replace that file with this standalone GJC bridge, which preserves the interview session across serialized follow-up answers, disposes it on GJC session switches, and drops queued predecessor-session starts. Download the example from immutable GJC commit `4311fefd49e9c6781c4d1111b8dd3f758e7d8974` and verify it before installation:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
curl -fL https://raw.githubusercontent.com/Yeachan-Heo/gajae-code/4311fefd49e9c6781c4d1111b8dd3f758e7d8974/packages/coding-agent/examples/extensions/ooo-bridge.ts -o /tmp/gjc-ooo-bridge.ts
|
|
32
|
+
shasum -a 256 /tmp/gjc-ooo-bridge.ts
|
|
33
|
+
mkdir -p "${HOME}/${GJC_CONFIG_DIR:-.gjc}/agent/extensions/ouroboros-ooo-bridge" && cp /tmp/gjc-ooo-bridge.ts "${HOME}/${GJC_CONFIG_DIR:-.gjc}/agent/extensions/ouroboros-ooo-bridge/index.ts"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The `shasum` output must match `2b0e1e25ac145331f112da629076875542db6f6e63c3c17adcd6770a4dcaf7bd` before the copy. The file has no runtime package imports and uses the host API injected by GJC, so compiled binaries do not require a peer `node_modules` directory beside the installation.
|
|
37
|
+
|
|
38
|
+
For a project-only installation, copy the same verified file to `.gjc/extensions/ouroboros-ooo-bridge/index.ts`. Start a new GJC session after installation, then enter:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
ooo interview "I want to build a task management CLI"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The first question is rendered in GJC. While that interview remains active, ordinary interactive input is sent as the answer with the same Ouroboros session ID; completion clears the correlation and returns subsequent ordinary prompts to GJC. Other `ooo ...` commands continue through `ouroboros dispatch --runtime gjc`, including exit-code `78` pass-through.
|
|
45
|
+
|
|
46
|
+
Set `OUROBOROS_CLI=/absolute/path/to/ouroboros` when the executable is outside `PATH`. Missing executable, MCP startup, and dispatch failures produce an error notification for the claimed input without preventing GJC startup or ordinary prompts.
|
|
47
|
+
|
|
48
|
+
This external path is separate from GJC's native `/skill:deep-interview`: the native skill runs GJC's bundled interview workflow, while `ooo interview` delegates to the installed Ouroboros MCP interview tool.
|
|
49
|
+
|
|
17
50
|
## Examples
|
|
18
51
|
|
|
19
52
|
### Custom Tools & API
|
|
@@ -39,10 +72,11 @@ cp packages/coding-agent/examples/extensions/pirate.ts .gjc/extensions/
|
|
|
39
72
|
|
|
40
73
|
### External Dependencies
|
|
41
74
|
|
|
42
|
-
| Extension | Description
|
|
43
|
-
| ----------------- |
|
|
44
|
-
| `chalk-logger.ts` | Uses chalk from parent node_modules (demonstrates jiti module resolution)
|
|
45
|
-
| `
|
|
75
|
+
| Extension | Description |
|
|
76
|
+
| ----------------- | ---------------------------------------------------------------------------- |
|
|
77
|
+
| `chalk-logger.ts` | Uses chalk from parent node_modules (demonstrates jiti module resolution) |
|
|
78
|
+
| `ooo-bridge.ts` | Opt-in `ooo ...` input bridge to the installed Ouroboros CLI and MCP runtime |
|
|
79
|
+
| `with-deps/` | Extension with its own package.json and dependencies |
|
|
46
80
|
|
|
47
81
|
## Writing Extensions
|
|
48
82
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface OooBridgeExtensionAPI {
|
|
2
|
+
pi: unknown;
|
|
3
|
+
on(event: "input" | "session_switch", handler: (event: unknown, context: unknown) => unknown): void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface OooBridgeHost {
|
|
7
|
+
createOuroborosOooBridge(): ((event: unknown, context: unknown) => unknown) & { reset(): Promise<void> };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function (pi: OooBridgeExtensionAPI) {
|
|
11
|
+
const host = pi.pi as OooBridgeHost;
|
|
12
|
+
const bridge = host.createOuroborosOooBridge();
|
|
13
|
+
pi.on("input", bridge);
|
|
14
|
+
pi.on("session_switch", () => bridge.reset());
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/coding-agent",
|
|
4
|
-
"version": "0.12.
|
|
4
|
+
"version": "0.12.12",
|
|
5
5
|
"description": "Gajae Code CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo and Gajae Code Contributors",
|
|
@@ -53,13 +53,13 @@
|
|
|
53
53
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
54
54
|
"@babel/parser": "^7.29.3",
|
|
55
55
|
"@mozilla/readability": "^0.6.0",
|
|
56
|
-
"@gajae-code/stats": "0.12.
|
|
57
|
-
"@gajae-code/agent-core": "0.12.
|
|
58
|
-
"@gajae-code/ai": "0.12.
|
|
59
|
-
"@gajae-code/bridge-client": "0.12.
|
|
60
|
-
"@gajae-code/natives": "0.12.
|
|
61
|
-
"@gajae-code/tui": "0.12.
|
|
62
|
-
"@gajae-code/utils": "0.12.
|
|
56
|
+
"@gajae-code/stats": "0.12.12",
|
|
57
|
+
"@gajae-code/agent-core": "0.12.12",
|
|
58
|
+
"@gajae-code/ai": "0.12.12",
|
|
59
|
+
"@gajae-code/bridge-client": "0.12.12",
|
|
60
|
+
"@gajae-code/natives": "0.12.12",
|
|
61
|
+
"@gajae-code/tui": "0.12.12",
|
|
62
|
+
"@gajae-code/utils": "0.12.12",
|
|
63
63
|
"@puppeteer/browsers": "^2.13.0",
|
|
64
64
|
"@types/turndown": "5.0.6",
|
|
65
65
|
"@xterm/headless": "^6.0.0",
|
|
@@ -37,6 +37,8 @@ const LOCKED_EXCLUSIONS: Readonly<Record<string, string>> = {
|
|
|
37
37
|
"agent_session:constructor": "internal accessor/plumbing, not a user-facing control seam",
|
|
38
38
|
"agent_session:registerToolSessionCleanup":
|
|
39
39
|
"internal tool lifecycle cleanup registration, not a user-facing SDK control seam",
|
|
40
|
+
"agent_session:registerToolSessionTransitionCleanup":
|
|
41
|
+
"internal tool transition cleanup registration for shared artifact-manager ownership, not a user-facing SDK control seam",
|
|
40
42
|
"agent_session:nextToolChoice": "internal accessor/plumbing, not a user-facing control seam",
|
|
41
43
|
"agent_session:setForcedToolChoice": "internal accessor/plumbing, not a user-facing control seam",
|
|
42
44
|
"agent_session:getActiveSkillState": "internal accessor/plumbing, not a user-facing control seam",
|
|
@@ -66,6 +68,8 @@ const LOCKED_EXCLUSIONS: Readonly<Record<string, string>> = {
|
|
|
66
68
|
"agent_session:closeWriterStrict": "internal ACP lifecycle teardown plumbing, not a user-facing control seam",
|
|
67
69
|
"agent_session:disposeChildSubprocesses": "internal accessor/plumbing, not a user-facing control seam",
|
|
68
70
|
"agent_session:waitForIdle": "internal accessor/plumbing, not a user-facing control seam",
|
|
71
|
+
"agent_session:extendStartupTurnBarrier":
|
|
72
|
+
"internal CLI startup-readiness fence composition, not a user-facing SDK control seam",
|
|
69
73
|
"agent_session:awaitPendingContextTransformations":
|
|
70
74
|
"internal context-transformation lifecycle barrier, not a user-facing SDK control seam",
|
|
71
75
|
"agent_session:drainAsyncJobDeliveriesForAcp": "internal accessor/plumbing, not a user-facing control seam",
|
package/src/async/job-manager.ts
CHANGED
|
@@ -463,6 +463,7 @@ export class AsyncJobManager {
|
|
|
463
463
|
readonly #terminalWaits = new Map<string, TerminalWaitState>();
|
|
464
464
|
#waitSeq = 0;
|
|
465
465
|
readonly #publishedTerminalGenerations = new Set<string>();
|
|
466
|
+
readonly #settledJobIds = new Set<string>();
|
|
466
467
|
#jobGenerationSeq = 0;
|
|
467
468
|
readonly #liveHandles = new Map<string, SubagentLiveHandle>();
|
|
468
469
|
readonly #subagentProgress = new Map<string, AgentProgress>();
|
|
@@ -820,6 +821,7 @@ export class AsyncJobManager {
|
|
|
820
821
|
|
|
821
822
|
this.#expireMonitorTombstones();
|
|
822
823
|
const id = this.#resolveJobId(options?.id);
|
|
824
|
+
this.#settledJobIds.delete(id);
|
|
823
825
|
const abortController = new AbortController();
|
|
824
826
|
const startTime = Date.now();
|
|
825
827
|
|
|
@@ -919,7 +921,9 @@ export class AsyncJobManager {
|
|
|
919
921
|
this.#scheduleEviction(id);
|
|
920
922
|
this.#drainResumeQueue();
|
|
921
923
|
}
|
|
922
|
-
})()
|
|
924
|
+
})().finally(() => {
|
|
925
|
+
this.#settledJobIds.add(id);
|
|
926
|
+
});
|
|
923
927
|
|
|
924
928
|
this.#jobs.set(id, job);
|
|
925
929
|
this.#notifyChange();
|
|
@@ -1218,6 +1222,8 @@ export class AsyncJobManager {
|
|
|
1218
1222
|
) {
|
|
1219
1223
|
continue;
|
|
1220
1224
|
}
|
|
1225
|
+
if (job.status === "cancelled" && this.#settledJobIds.has(job.id) && !this.#subagentRecords.has(subagentId))
|
|
1226
|
+
continue;
|
|
1221
1227
|
if (!targets.has(subagentId)) {
|
|
1222
1228
|
targets.set(subagentId, { subagentId, jobId: job.id, source: "metadata_job" });
|
|
1223
1229
|
}
|
|
@@ -1298,7 +1304,7 @@ export class AsyncJobManager {
|
|
|
1298
1304
|
lease.targets.map(target => target.subagentId),
|
|
1299
1305
|
);
|
|
1300
1306
|
}
|
|
1301
|
-
if (state.phase === "proved" && state.proof) return state.proof;
|
|
1307
|
+
if (state.phase === "proved" && state.proof?.confirmed) return state.proof;
|
|
1302
1308
|
state.phase = "proving";
|
|
1303
1309
|
const settled = new Set<string>();
|
|
1304
1310
|
const promises: Promise<void>[] = [];
|
|
@@ -2064,6 +2070,7 @@ export class AsyncJobManager {
|
|
|
2064
2070
|
this.#purgeTerminalSubagentStateForJob(jobId);
|
|
2065
2071
|
const job = this.#jobs.get(jobId);
|
|
2066
2072
|
this.#jobs.delete(jobId);
|
|
2073
|
+
this.#settledJobIds.delete(jobId);
|
|
2067
2074
|
this.#lifecycles.delete(jobId);
|
|
2068
2075
|
this.#lifecyclePhases.delete(jobId);
|
|
2069
2076
|
this.#deadLetteredDeliveries.delete(jobId);
|
package/src/cli/notify-cli.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
7
7
|
import { APP_NAME } from "@gajae-code/utils/dirs";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import { Settings, type SettingsAtomicPatch } from "../config/settings";
|
|
10
|
+
import { SessionIndex } from "../sdk/broker/session-index";
|
|
10
11
|
import {
|
|
11
12
|
ChatDaemonController,
|
|
12
13
|
type EnsureChatDaemonResult,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
ensureSlackDaemon,
|
|
15
16
|
} from "../sdk/bus/chat-daemon-control";
|
|
16
17
|
import { getNotificationConfig, maskToken, tokenFingerprint } from "../sdk/bus/config";
|
|
18
|
+
import { type ActivatedPreparedSession, activatePreparedSession } from "../sdk/bus/existing-thread-readiness";
|
|
17
19
|
import {
|
|
18
20
|
clearTelegramActivationMarker,
|
|
19
21
|
createTelegramActivationMarker,
|
|
@@ -36,6 +38,12 @@ import {
|
|
|
36
38
|
sanitizeDiagnostic,
|
|
37
39
|
sendNotificationTest,
|
|
38
40
|
} from "../sdk/bus/notification-service";
|
|
41
|
+
import {
|
|
42
|
+
type BoundSlackThread,
|
|
43
|
+
bindConfiguredSlackThread,
|
|
44
|
+
isBoundedSlackRootTs,
|
|
45
|
+
SlackThreadBindingError,
|
|
46
|
+
} from "../sdk/bus/slack-thread-binding";
|
|
39
47
|
import {
|
|
40
48
|
type EnsureTelegramDaemonDetailedResult,
|
|
41
49
|
ensureTelegramDaemonRunningDetailed,
|
|
@@ -49,7 +57,15 @@ import {
|
|
|
49
57
|
type TelegramSetupTimers,
|
|
50
58
|
} from "../sdk/bus/telegram-setup";
|
|
51
59
|
|
|
52
|
-
export type NotifyAction =
|
|
60
|
+
export type NotifyAction =
|
|
61
|
+
| "setup"
|
|
62
|
+
| "status"
|
|
63
|
+
| "health"
|
|
64
|
+
| "test"
|
|
65
|
+
| "recovery"
|
|
66
|
+
| "bind-thread"
|
|
67
|
+
| "activate-thread"
|
|
68
|
+
| "daemon-internal";
|
|
53
69
|
export type NotifySetupProvider = "telegram" | "discord" | "slack";
|
|
54
70
|
|
|
55
71
|
export interface NotifyCommandArgs {
|
|
@@ -72,6 +88,8 @@ export interface NotifyCommandArgs {
|
|
|
72
88
|
forceDaemonLock?: boolean;
|
|
73
89
|
probe?: boolean;
|
|
74
90
|
message?: string;
|
|
91
|
+
sessionId?: string;
|
|
92
|
+
threadTs?: string;
|
|
75
93
|
}
|
|
76
94
|
|
|
77
95
|
export interface NotifyCommandDeps {
|
|
@@ -99,6 +117,8 @@ export interface NotifyCommandDeps {
|
|
|
99
117
|
setupPidIncarnation?: (pid: number) => string | undefined;
|
|
100
118
|
ensureProviderDaemon?: (provider: "discord" | "slack", settings: Settings) => Promise<EnsureChatDaemonResult>;
|
|
101
119
|
ensureTelegramDaemon?: (settings: Settings) => Promise<EnsureTelegramDaemonDetailedResult>;
|
|
120
|
+
bindSlackThread?: (input: { settings: Settings; sessionId: string; threadTs: string }) => Promise<BoundSlackThread>;
|
|
121
|
+
activatePreparedSession?: (input: { settings: Settings; sessionId: string }) => Promise<ActivatedPreparedSession>;
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
|
|
@@ -200,6 +220,23 @@ export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
|
|
|
200
220
|
? { action, rawArgs: args.slice(2), forceDaemonLock: flags.get("--force-daemon-lock") === true }
|
|
201
221
|
: undefined;
|
|
202
222
|
}
|
|
223
|
+
if (action === "bind-thread") {
|
|
224
|
+
const rest = args.slice(2);
|
|
225
|
+
const flags = parseFlags(rest, new Set(["--session-id", "--thread-ts"]), new Set());
|
|
226
|
+
if (!flags) return undefined;
|
|
227
|
+
const sessionId = flags.get("--session-id");
|
|
228
|
+
const threadTs = flags.get("--thread-ts");
|
|
229
|
+
if (typeof sessionId !== "string" || typeof threadTs !== "string") return undefined;
|
|
230
|
+
return { action, rawArgs: rest, sessionId, threadTs };
|
|
231
|
+
}
|
|
232
|
+
if (action === "activate-thread") {
|
|
233
|
+
const rest = args.slice(2);
|
|
234
|
+
const flags = parseFlags(rest, new Set(["--session-id"]), new Set());
|
|
235
|
+
if (!flags) return undefined;
|
|
236
|
+
const sessionId = flags.get("--session-id");
|
|
237
|
+
if (typeof sessionId !== "string") return undefined;
|
|
238
|
+
return { action, rawArgs: rest, sessionId };
|
|
239
|
+
}
|
|
203
240
|
if (action === "daemon-internal") {
|
|
204
241
|
return {
|
|
205
242
|
action,
|
|
@@ -232,6 +269,12 @@ export async function runNotifyCommand(cmd: NotifyCommandArgs, deps: NotifyComma
|
|
|
232
269
|
case "recovery":
|
|
233
270
|
await runRecovery(deps, cmd.forceDaemonLock);
|
|
234
271
|
return;
|
|
272
|
+
case "bind-thread":
|
|
273
|
+
await runBindThread(cmd, deps);
|
|
274
|
+
return;
|
|
275
|
+
case "activate-thread":
|
|
276
|
+
await runActivateThread(cmd, deps);
|
|
277
|
+
return;
|
|
235
278
|
case "daemon-internal":
|
|
236
279
|
if (cmd.smoke) {
|
|
237
280
|
await runDaemonInternal(["--smoke"]);
|
|
@@ -457,6 +500,7 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
457
500
|
process.stdout.write(`Using provided chat id ${result.chatId} (non-interactive).\n`);
|
|
458
501
|
}
|
|
459
502
|
let settingsCommitted = false;
|
|
503
|
+
let commitAttempted = false;
|
|
460
504
|
try {
|
|
461
505
|
const proposedIdentity = deps.setupPreflight
|
|
462
506
|
? proposedIdentityFromSetupPreflight(deps.setupPreflight, token.trim(), result.chatId)
|
|
@@ -480,6 +524,7 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
480
524
|
{ path: "notifications.telegram.enabled", op: "set", value: true },
|
|
481
525
|
];
|
|
482
526
|
if (deps.setupRedact ?? cmd.redact) patches.push({ path: "notifications.redact", op: "set", value: true });
|
|
527
|
+
commitAttempted = true;
|
|
483
528
|
const receipt = await settings.commitAtomicBatch(patches);
|
|
484
529
|
settingsCommitted = true;
|
|
485
530
|
const activationMarker = createTelegramActivationMarker({
|
|
@@ -534,8 +579,24 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
534
579
|
receipt.discard();
|
|
535
580
|
} catch (error) {
|
|
536
581
|
const detail = sanitizeDiagnostic(error instanceof Error ? error.message : "unknown persistence failure", token);
|
|
582
|
+
// The wording must describe what a follow-up `notify status` will show. A failure
|
|
583
|
+
// raised after the durable write landed — including one raised from inside the
|
|
584
|
+
// commit itself — must not claim the settings were not persisted, or the operator
|
|
585
|
+
// walks away believing Telegram is off while the daemon is armed for that token.
|
|
586
|
+
// Observed state wins; the code-path flag is only the fallback for an unreadable read.
|
|
587
|
+
const observed = telegramIntentIsPersisted(settings, token.trim(), result.chatId);
|
|
588
|
+
const persisted = observed ?? settingsCommitted;
|
|
589
|
+
// A commit that was entered and then failed, whose durable state is also unreadable,
|
|
590
|
+
// is genuinely undecided: `commitAtomicBatch` can persist and still throw. Claiming
|
|
591
|
+
// either outcome would be a guess, so say so and point at the authoritative check.
|
|
592
|
+
if (!persisted && observed === undefined && commitAttempted) {
|
|
593
|
+
throw new Error(
|
|
594
|
+
"Telegram notification settings may or may not have been saved, and the stored configuration could not be read; " +
|
|
595
|
+
`run \`gjc notify status\` before retrying: ${detail}`,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
537
598
|
throw new Error(
|
|
538
|
-
|
|
599
|
+
persisted
|
|
539
600
|
? `Telegram notification settings were saved, but activation or recovery failed: ${detail}`
|
|
540
601
|
: `Unable to persist and activate Telegram notification settings: ${detail}`,
|
|
541
602
|
);
|
|
@@ -545,6 +606,26 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
545
606
|
);
|
|
546
607
|
}
|
|
547
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Whether the durable settings already carry the Telegram intent this setup run attempted to
|
|
611
|
+
* write: the same identity *and* the enabled state it would have produced. Matching the token
|
|
612
|
+
* and chat id alone is not enough — a previously disabled configuration can already hold both,
|
|
613
|
+
* and a commit that fails before enabling Telegram has persisted nothing new.
|
|
614
|
+
*
|
|
615
|
+
* Returns `undefined` when the durable state cannot be observed, so the caller can fall back
|
|
616
|
+
* instead of reporting a state nobody read.
|
|
617
|
+
*/
|
|
618
|
+
function telegramIntentIsPersisted(settings: Settings, botToken: string, chatId: string): boolean | undefined {
|
|
619
|
+
try {
|
|
620
|
+
const cfg = getNotificationConfig(settings);
|
|
621
|
+
return (
|
|
622
|
+
cfg.enabled === true && cfg.telegram?.enabled === true && cfg.botToken === botToken && cfg.chatId === chatId
|
|
623
|
+
);
|
|
624
|
+
} catch {
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
548
629
|
function proposedIdentityFromSetupPreflight(
|
|
549
630
|
preflight: TelegramSetupPreflight,
|
|
550
631
|
botToken: string,
|
|
@@ -728,6 +809,143 @@ async function runRecovery(deps: NotifyCommandDeps, forceDaemonLock = false): Pr
|
|
|
728
809
|
process.stdout.write(`${formatNotificationRecoveryReport(report)}\n`);
|
|
729
810
|
}
|
|
730
811
|
|
|
812
|
+
/** Target and credential inputs stay owned by `notify setup`; binding never re-routes a session elsewhere. */
|
|
813
|
+
const BIND_THREAD_REJECTED_INPUTS: readonly (keyof NotifyCommandArgs)[] = [
|
|
814
|
+
"provider",
|
|
815
|
+
"token",
|
|
816
|
+
"chatId",
|
|
817
|
+
"discordBotToken",
|
|
818
|
+
"discordApplicationId",
|
|
819
|
+
"discordGuildId",
|
|
820
|
+
"discordParentChannelId",
|
|
821
|
+
"slackBotToken",
|
|
822
|
+
"slackAppToken",
|
|
823
|
+
"slackWorkspaceId",
|
|
824
|
+
"slackChannelId",
|
|
825
|
+
"slackAuthorizedUserId",
|
|
826
|
+
"message",
|
|
827
|
+
"probe",
|
|
828
|
+
"redact",
|
|
829
|
+
"forceDaemonLock",
|
|
830
|
+
"smoke",
|
|
831
|
+
];
|
|
832
|
+
|
|
833
|
+
export interface BindThreadInvocation {
|
|
834
|
+
sessionId: string;
|
|
835
|
+
threadTs: string;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Enforce the exact `bind-thread` grammar at every entrypoint.
|
|
840
|
+
*
|
|
841
|
+
* The command accepts only a session and a root; a positional argument, an
|
|
842
|
+
* unrelated notify flag, or a target/credential input is a rejection rather than
|
|
843
|
+
* something silently ignored, so no other invocation shape can reach the
|
|
844
|
+
* binding authority.
|
|
845
|
+
*/
|
|
846
|
+
export function assertStrictBindThreadInvocation(cmd: NotifyCommandArgs): BindThreadInvocation {
|
|
847
|
+
const rejected = BIND_THREAD_REJECTED_INPUTS.filter(key => {
|
|
848
|
+
const value = cmd[key];
|
|
849
|
+
return value !== undefined && value !== false && value !== "";
|
|
850
|
+
});
|
|
851
|
+
if (rejected.length > 0)
|
|
852
|
+
throw new Error(
|
|
853
|
+
`notify bind-thread accepts only --session-id and --thread-ts (rejected: ${rejected.join(", ")}).`,
|
|
854
|
+
);
|
|
855
|
+
const { sessionId, threadTs } = cmd;
|
|
856
|
+
if (!sessionId || !threadTs) throw new Error("notify bind-thread requires --session-id and --thread-ts.");
|
|
857
|
+
const allowed = new Set(["--session-id", sessionId, "--thread-ts", threadTs]);
|
|
858
|
+
const stray = cmd.rawArgs.filter(token => !allowed.has(token));
|
|
859
|
+
if (stray.length > 0)
|
|
860
|
+
throw new Error(`notify bind-thread does not accept additional arguments (rejected: ${stray.join(", ")}).`);
|
|
861
|
+
if (!isBoundedSlackRootTs(threadTs))
|
|
862
|
+
throw new SlackThreadBindingError(
|
|
863
|
+
"invalid_root",
|
|
864
|
+
"Slack root timestamp must be a bounded <seconds>.<fraction> message timestamp.",
|
|
865
|
+
);
|
|
866
|
+
return { sessionId, threadTs };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/** Adopt an existing Slack thread for a live session; the operator supplies only session and root identity. */
|
|
870
|
+
async function runBindThread(cmd: NotifyCommandArgs, deps: NotifyCommandDeps): Promise<void> {
|
|
871
|
+
const { sessionId, threadTs } = assertStrictBindThreadInvocation(cmd);
|
|
872
|
+
const bind = deps.bindSlackThread ?? (input => bindConfiguredSlackThread(input));
|
|
873
|
+
const bound = await bind({ settings: await getSettings(deps), sessionId, threadTs });
|
|
874
|
+
process.stdout.write(`${formatBoundSlackThread(bound)}\n`);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** Confirmation carries identifiers only: no tokens, message bodies, or control secrets. */
|
|
878
|
+
export function formatBoundSlackThread(bound: BoundSlackThread): string {
|
|
879
|
+
return [
|
|
880
|
+
`${chalk.green("Bound")} Slack thread for session ${bound.sessionId}`,
|
|
881
|
+
` session generation: ${bound.endpointGeneration}`,
|
|
882
|
+
` workspace/channel: ${bound.teamId}/${bound.channelId}`,
|
|
883
|
+
` thread root: ${bound.rootTs}`,
|
|
884
|
+
` daemon owner: ${bound.ownerId} (generation ${bound.daemonGeneration})`,
|
|
885
|
+
].join("\n");
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/** Activation carries only a session; a root or target here is a rejection, not an override. */
|
|
889
|
+
const ACTIVATE_THREAD_REJECTED_INPUTS: readonly (keyof NotifyCommandArgs)[] = [
|
|
890
|
+
...BIND_THREAD_REJECTED_INPUTS,
|
|
891
|
+
"threadTs",
|
|
892
|
+
];
|
|
893
|
+
|
|
894
|
+
export interface ActivateThreadInvocation {
|
|
895
|
+
sessionId: string;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Enforce the exact `activate-thread` grammar at every entrypoint.
|
|
900
|
+
*
|
|
901
|
+
* Activation names one prepared session and nothing else: the root it adopts is
|
|
902
|
+
* already the applied binding, so a supplied root, target, or credential is a
|
|
903
|
+
* rejection rather than something silently ignored.
|
|
904
|
+
*/
|
|
905
|
+
export function assertStrictActivateThreadInvocation(cmd: NotifyCommandArgs): ActivateThreadInvocation {
|
|
906
|
+
const rejected = ACTIVATE_THREAD_REJECTED_INPUTS.filter(key => {
|
|
907
|
+
const value = cmd[key];
|
|
908
|
+
return value !== undefined && value !== false && value !== "";
|
|
909
|
+
});
|
|
910
|
+
if (rejected.length > 0)
|
|
911
|
+
throw new Error(`notify activate-thread accepts only --session-id (rejected: ${rejected.join(", ")}).`);
|
|
912
|
+
const { sessionId } = cmd;
|
|
913
|
+
if (!sessionId) throw new Error("notify activate-thread requires --session-id.");
|
|
914
|
+
const allowed = new Set(["--session-id", sessionId]);
|
|
915
|
+
const stray = cmd.rawArgs.filter(token => !allowed.has(token));
|
|
916
|
+
if (stray.length > 0)
|
|
917
|
+
throw new Error(`notify activate-thread does not accept additional arguments (rejected: ${stray.join(", ")}).`);
|
|
918
|
+
return { sessionId };
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Publish the readiness a prepared session withheld.
|
|
923
|
+
*
|
|
924
|
+
* The session's own host owns the decision: this command only proves discovery
|
|
925
|
+
* authority and asks it to activate, so activation before a binding exists is
|
|
926
|
+
* refused by the session rather than forced by the operator.
|
|
927
|
+
*/
|
|
928
|
+
async function runActivateThread(cmd: NotifyCommandArgs, deps: NotifyCommandDeps): Promise<void> {
|
|
929
|
+
const { sessionId } = assertStrictActivateThreadInvocation(cmd);
|
|
930
|
+
const activate =
|
|
931
|
+
deps.activatePreparedSession ??
|
|
932
|
+
(async (input: { settings: Settings; sessionId: string }) =>
|
|
933
|
+
await activatePreparedSession({
|
|
934
|
+
sessionIndex: await new SessionIndex(input.settings.getAgentDir()).open(),
|
|
935
|
+
sessionId: input.sessionId,
|
|
936
|
+
}));
|
|
937
|
+
const activated = await activate({ settings: await getSettings(deps), sessionId });
|
|
938
|
+
process.stdout.write(`${formatActivatedSession(activated)}\n`);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** Confirmation carries identifiers only: no endpoints, tokens, or thread content. */
|
|
942
|
+
export function formatActivatedSession(activated: ActivatedPreparedSession): string {
|
|
943
|
+
return [
|
|
944
|
+
`${chalk.green("Activated")} session ${activated.sessionId} (${activated.status})`,
|
|
945
|
+
` session generation: ${activated.endpointGeneration}`,
|
|
946
|
+
].join("\n");
|
|
947
|
+
}
|
|
948
|
+
|
|
731
949
|
export function printNotifyHelp(): void {
|
|
732
950
|
process.stdout.write(`${chalk.bold(`${APP_NAME} notify`)} - Configure Telegram, Discord, or Slack notifications
|
|
733
951
|
|
|
@@ -744,6 +962,8 @@ ${chalk.bold("Usage:")}
|
|
|
744
962
|
${APP_NAME} notify health [--provider telegram|discord|slack] [--probe]
|
|
745
963
|
${APP_NAME} notify test [--provider telegram|discord|slack] [--message <text>]
|
|
746
964
|
${APP_NAME} notify recovery [--force-daemon-lock]
|
|
965
|
+
${APP_NAME} notify bind-thread --session-id <sessionId> --thread-ts <rootTs>
|
|
966
|
+
${APP_NAME} notify activate-thread --session-id <sessionId>
|
|
747
967
|
|
|
748
968
|
${chalk.bold("Subcommands:")}
|
|
749
969
|
setup Pair Telegram or atomically save and activate complete Discord/Slack settings
|
|
@@ -751,6 +971,8 @@ ${chalk.bold("Subcommands:")}
|
|
|
751
971
|
health Report selected provider state; --probe uses REST only and never opens Gateway/Socket Mode
|
|
752
972
|
test Send a one-off test through one selected or uniquely effective provider
|
|
753
973
|
recovery Clear dead-owner daemon locks and stale per-session endpoint files (never touches a live owner); --force-daemon-lock retries only with the same fail-closed dead-owner proof
|
|
974
|
+
bind-thread Adopt an existing Slack thread as a live session's root; target and credentials come from setup only
|
|
975
|
+
activate-thread Publish the readiness a prepared session withheld once its thread binding is applied
|
|
754
976
|
|
|
755
977
|
${chalk.bold("Examples:")}
|
|
756
978
|
${APP_NAME} notify setup
|
|
@@ -761,6 +983,8 @@ ${chalk.bold("Examples:")}
|
|
|
761
983
|
${APP_NAME} notify health --provider discord --probe
|
|
762
984
|
${APP_NAME} notify test --provider slack --message "hello from gjc"
|
|
763
985
|
${APP_NAME} notify recovery
|
|
986
|
+
${APP_NAME} notify bind-thread --session-id 01J... --thread-ts 1785573662.132329
|
|
987
|
+
${APP_NAME} notify activate-thread --session-id 01J...
|
|
764
988
|
|
|
765
989
|
${chalk.bold("Threaded Mode:")}
|
|
766
990
|
GJC uses Telegram private-chat topics for per-session threads. Setup verifies the bot
|