@gajae-code/coding-agent 0.12.5 → 0.12.7
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 +34 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -3
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +43 -0
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/async/job-manager.ts +430 -46
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +360 -156
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/modes/acp/acp-agent.ts +27 -13
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +691 -92
- package/src/sdk/bus/chat-daemon-control.ts +30 -6
- package/src/sdk/bus/index.ts +23 -6
- package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/session.ts +7 -6
- package/src/session/agent-session.ts +752 -339
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/internal/managed-session-scope.ts +252 -67
- package/src/session/internal/managed-session-storage.ts +1 -0
- package/src/session/session-manager.ts +7 -0
- package/src/session/session-storage.ts +146 -24
- package/src/task/index.ts +230 -19
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +2 -0
- package/src/task/types.ts +10 -0
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +10 -2
- package/src/tools/subagent.ts +97 -75
- package/src/tools/todo-write.ts +47 -0
|
@@ -80,10 +80,32 @@ async function listFilesRecursively(rootPath: string): Promise<string[]> {
|
|
|
80
80
|
|
|
81
81
|
return files.sort((a, b) => a.localeCompare(b));
|
|
82
82
|
}
|
|
83
|
+
function localListingLabel(file: string): string {
|
|
84
|
+
return file.replace(/[\\[\]\r\n]/g, character => {
|
|
85
|
+
switch (character) {
|
|
86
|
+
case "\r":
|
|
87
|
+
return "\\r";
|
|
88
|
+
case "\n":
|
|
89
|
+
return "\\n";
|
|
90
|
+
default:
|
|
91
|
+
return `\\${character}`;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function localListingHref(file: string): string {
|
|
97
|
+
return file
|
|
98
|
+
.split("/")
|
|
99
|
+
.map(component => encodeURIComponent(component).replaceAll("(", "%28").replaceAll(")", "%29"))
|
|
100
|
+
.join("/");
|
|
101
|
+
}
|
|
83
102
|
|
|
84
103
|
async function buildListing(url: InternalUrl, localRoot: string): Promise<InternalResource> {
|
|
85
104
|
const files = await listFilesRecursively(localRoot);
|
|
86
|
-
const listing =
|
|
105
|
+
const listing =
|
|
106
|
+
files.length === 0
|
|
107
|
+
? "(empty)"
|
|
108
|
+
: files.map(file => `- [${localListingLabel(file)}](local://${localListingHref(file)})`).join("\n");
|
|
87
109
|
const content =
|
|
88
110
|
`# Local\n\n` +
|
|
89
111
|
`Session-scoped scratch space for large intermediate data, subagent handoffs, and reusable planning artifacts.\n\n` +
|
|
@@ -103,31 +125,30 @@ async function buildListing(url: InternalUrl, localRoot: string): Promise<Intern
|
|
|
103
125
|
function extractRelativePath(url: InternalUrl): string {
|
|
104
126
|
const host = url.rawHost || url.hostname;
|
|
105
127
|
const pathname = url.rawPathname ?? url.pathname;
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
? `${host}${pathname}`
|
|
110
|
-
: host
|
|
111
|
-
: pathname && pathname !== "/"
|
|
112
|
-
? pathname.slice(1)
|
|
113
|
-
: "";
|
|
114
|
-
|
|
115
|
-
if (!combined) {
|
|
116
|
-
return "";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let decoded: string;
|
|
128
|
+
const encodedAuthority = /^local:\/\/([^/?#]*)/i.exec(url.href)?.[1] ?? "";
|
|
129
|
+
const validatedHost = host;
|
|
130
|
+
let decodedPathname = pathname;
|
|
120
131
|
try {
|
|
121
|
-
|
|
132
|
+
const decodedAuthority = decodeURIComponent(encodedAuthority);
|
|
133
|
+
if (host && decodedAuthority !== host) throw new Error("authority_mismatch");
|
|
134
|
+
decodedPathname = decodeURIComponent(pathname.replaceAll("\\", "/"));
|
|
122
135
|
} catch {
|
|
123
136
|
throw new Error(`Invalid URL encoding in local:// path: ${url.href}`);
|
|
124
137
|
}
|
|
138
|
+
const combined = validatedHost
|
|
139
|
+
? decodedPathname && decodedPathname !== "/"
|
|
140
|
+
? `${validatedHost}${decodedPathname}`
|
|
141
|
+
: validatedHost
|
|
142
|
+
: decodedPathname && decodedPathname !== "/"
|
|
143
|
+
? decodedPathname.slice(1)
|
|
144
|
+
: "";
|
|
145
|
+
if (!combined) return "";
|
|
125
146
|
try {
|
|
126
|
-
validateRelativePath(
|
|
147
|
+
validateRelativePath(combined);
|
|
127
148
|
} catch (error) {
|
|
128
149
|
throw toLocalValidationError(error);
|
|
129
150
|
}
|
|
130
|
-
return
|
|
151
|
+
return combined;
|
|
131
152
|
}
|
|
132
153
|
|
|
133
154
|
function safeSessionId(options: LocalProtocolOptions): string {
|
|
@@ -718,6 +718,7 @@ export class AcpAgent implements Agent {
|
|
|
718
718
|
readonly #resolvingExisting = new Map<string, PendingAttachment>();
|
|
719
719
|
readonly #knownSessionCwds = new Map<string, string>();
|
|
720
720
|
readonly #knownSessionMetadata = new Map<string, { title?: string; updatedAt?: string }>();
|
|
721
|
+
readonly #pendingDeleteLocators = new Map<string, { cwd: string; path: string }>();
|
|
721
722
|
readonly #pendingCloseIdempotencyKeys = new Map<string, string>();
|
|
722
723
|
readonly #sessionEpochs = new Map<string, number>();
|
|
723
724
|
readonly #tearingDown = new Map<string, number>();
|
|
@@ -918,24 +919,35 @@ export class AcpAgent implements Agent {
|
|
|
918
919
|
|
|
919
920
|
async deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse> {
|
|
920
921
|
const record = this.#sessions.get(params.sessionId);
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
//
|
|
924
|
-
|
|
922
|
+
const pendingLocator = this.#pendingDeleteLocators.get(params.sessionId);
|
|
923
|
+
const cwd = record?.cwd ?? this.#knownSessionCwds.get(params.sessionId) ?? pendingLocator?.cwd;
|
|
924
|
+
// ACP's delete request has no cwd. Unknown ids remain the protocol no-op,
|
|
925
|
+
// while the broker can reconstruct an authenticated pending locator from its durable ledger.
|
|
926
|
+
if (!cwd) {
|
|
927
|
+
await (await this.#brokerAdapter()).global(
|
|
928
|
+
"session.delete",
|
|
929
|
+
{ sessionId: params.sessionId },
|
|
930
|
+
this.#lifecycleIdempotencyKey(params.sessionId, "session.delete"),
|
|
931
|
+
);
|
|
932
|
+
return {};
|
|
933
|
+
}
|
|
925
934
|
this.#beginTeardown(params.sessionId);
|
|
926
935
|
try {
|
|
927
936
|
await this.#teardownSession(params.sessionId, "deleted", true);
|
|
928
|
-
let saved:
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
937
|
+
let saved = pendingLocator?.cwd === cwd ? pendingLocator.path : undefined;
|
|
938
|
+
if (!saved) {
|
|
939
|
+
try {
|
|
940
|
+
saved = await this.#resolveSavedSession(params.sessionId, cwd);
|
|
941
|
+
} catch (error) {
|
|
942
|
+
if (error instanceof AcpSdkAdapterError && error.code === "not_found") {
|
|
943
|
+
this.#knownSessionCwds.delete(params.sessionId);
|
|
944
|
+
this.#knownSessionMetadata.delete(params.sessionId);
|
|
945
|
+
return {};
|
|
946
|
+
}
|
|
947
|
+
throw error;
|
|
936
948
|
}
|
|
937
|
-
throw error;
|
|
938
949
|
}
|
|
950
|
+
this.#pendingDeleteLocators.set(params.sessionId, { cwd, path: saved });
|
|
939
951
|
await (await this.#brokerAdapter()).global(
|
|
940
952
|
"session.delete",
|
|
941
953
|
{ sessionId: params.sessionId, sessionPath: saved, cwd, target: { path: cwd } },
|
|
@@ -943,6 +955,7 @@ export class AcpAgent implements Agent {
|
|
|
943
955
|
);
|
|
944
956
|
this.#knownSessionCwds.delete(params.sessionId);
|
|
945
957
|
this.#knownSessionMetadata.delete(params.sessionId);
|
|
958
|
+
this.#pendingDeleteLocators.delete(params.sessionId);
|
|
946
959
|
return {};
|
|
947
960
|
} finally {
|
|
948
961
|
this.#finishTeardown(params.sessionId);
|
|
@@ -2168,6 +2181,7 @@ export class AcpAgent implements Agent {
|
|
|
2168
2181
|
this.#resolvingExisting.clear();
|
|
2169
2182
|
this.#knownSessionCwds.clear();
|
|
2170
2183
|
this.#knownSessionMetadata.clear();
|
|
2184
|
+
this.#pendingDeleteLocators.clear();
|
|
2171
2185
|
this.#pendingCloseIdempotencyKeys.clear();
|
|
2172
2186
|
if (this.#closing.size === 0) this.#closing.clear();
|
|
2173
2187
|
this.#tearingDown.clear();
|
|
@@ -56,6 +56,7 @@ export interface GjcBundleSettingsDependencies {
|
|
|
56
56
|
export interface GjcBundleSettingsCallbacks {
|
|
57
57
|
onClose: () => void;
|
|
58
58
|
onBundlesChanged?: () => void;
|
|
59
|
+
onRenderRequested?: () => void;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
export type GjcBundleSettingsState =
|
|
@@ -187,6 +188,7 @@ export class GjcBundleSettingsComponent extends Container {
|
|
|
187
188
|
),
|
|
188
189
|
);
|
|
189
190
|
this.addChild(new DynamicBorder());
|
|
191
|
+
this.callbacks.onRenderRequested?.();
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
#renderList(): void {
|
|
@@ -127,6 +127,7 @@ export interface PluginDetailCallbacks {
|
|
|
127
127
|
onFeatureChange: (feature: string, enabled: boolean) => void;
|
|
128
128
|
onConfigChange: (key: string, value: unknown) => void;
|
|
129
129
|
onBack: () => void;
|
|
130
|
+
onRenderRequested?: () => void;
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
/**
|
|
@@ -288,6 +289,7 @@ export class PluginDetailComponent extends Container {
|
|
|
288
289
|
this.addChild(new Spacer(1));
|
|
289
290
|
this.addChild(new Text(theme.fg("dim", " Enter to edit · Esc to go back"), 0, 0));
|
|
290
291
|
this.addChild(new DynamicBorder());
|
|
292
|
+
this.callbacks.onRenderRequested?.();
|
|
291
293
|
}
|
|
292
294
|
|
|
293
295
|
handleInput(data: string): void {
|
|
@@ -409,6 +411,7 @@ class ConfigInputSubmenu extends Container {
|
|
|
409
411
|
export interface PluginSettingsCallbacks {
|
|
410
412
|
onClose: () => void;
|
|
411
413
|
onPluginChanged: () => void;
|
|
414
|
+
onRenderRequested?: () => void;
|
|
412
415
|
}
|
|
413
416
|
|
|
414
417
|
/** Component with handleInput method */
|
|
@@ -450,6 +453,7 @@ export class PluginSettingsComponent extends Container {
|
|
|
450
453
|
});
|
|
451
454
|
|
|
452
455
|
this.addChild(this.#viewComponent);
|
|
456
|
+
this.callbacks.onRenderRequested?.();
|
|
453
457
|
}
|
|
454
458
|
|
|
455
459
|
#showPluginDetail(plugin: InstalledPlugin): void {
|
|
@@ -477,6 +481,7 @@ export class PluginSettingsComponent extends Container {
|
|
|
477
481
|
this.callbacks.onPluginChanged();
|
|
478
482
|
},
|
|
479
483
|
onBack: () => this.#showPluginList(),
|
|
484
|
+
onRenderRequested: () => this.callbacks.onRenderRequested?.(),
|
|
480
485
|
});
|
|
481
486
|
|
|
482
487
|
this.addChild(this.#viewComponent);
|
|
@@ -719,6 +719,8 @@ export interface SettingsCallbacks {
|
|
|
719
719
|
getStatusLinePreview?: (width?: number) => string;
|
|
720
720
|
/** Called when plugins change */
|
|
721
721
|
onPluginsChanged?: () => void;
|
|
722
|
+
/** Called when asynchronously rebuilt settings content needs a repaint. */
|
|
723
|
+
onRenderRequested?: () => void;
|
|
722
724
|
/** Called when an interactive setting cannot be committed. */
|
|
723
725
|
onError?: (message: string) => void;
|
|
724
726
|
/** Called when settings panel is closed */
|
|
@@ -1287,6 +1289,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1287
1289
|
this.#pluginComponent = new PluginSettingsComponent(this.context.cwd, {
|
|
1288
1290
|
onClose: () => this.callbacks.onCancel(),
|
|
1289
1291
|
onPluginChanged: () => this.callbacks.onPluginsChanged?.(),
|
|
1292
|
+
onRenderRequested: () => this.callbacks.onRenderRequested?.(),
|
|
1290
1293
|
});
|
|
1291
1294
|
this.addChild(this.#pluginComponent);
|
|
1292
1295
|
}
|
|
@@ -1296,6 +1299,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1296
1299
|
{
|
|
1297
1300
|
onClose: () => this.callbacks.onCancel(),
|
|
1298
1301
|
onBundlesChanged: () => this.callbacks.onPluginsChanged?.(),
|
|
1302
|
+
onRenderRequested: () => this.callbacks.onRenderRequested?.(),
|
|
1299
1303
|
},
|
|
1300
1304
|
{
|
|
1301
1305
|
runtimeSnapshotProvider: this.context.gjcRuntimeSnapshot,
|
|
@@ -1334,6 +1334,7 @@ export class SelectorController {
|
|
|
1334
1334
|
onPluginsChanged: () => {
|
|
1335
1335
|
this.ctx.ui.requestRender();
|
|
1336
1336
|
},
|
|
1337
|
+
onRenderRequested: () => this.ctx.ui.requestRender(),
|
|
1337
1338
|
onCancel: () => {
|
|
1338
1339
|
done();
|
|
1339
1340
|
// Restore status line to saved settings
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
Start a background monitor that streams events from a long-running script. Each stdout line is
|
|
1
|
+
Start a background monitor that streams events from a long-running script. Each stdout line is captured; persistent notifications are latest-biased and coalesced over a short debounce window, while terminal completion flushes the newest pending line. Events arrive on their own schedule and are not replies from the user, even if one lands while you're waiting for the user to answer a question.
|
|
2
2
|
|
|
3
3
|
Pick by how many notifications you need:
|
|
4
4
|
- **One** ("tell me when the server is ready / the build finishes") → use `bash` with `async: true`. That returns a single completion notification when the command exits.
|
|
5
|
-
- **Many ongoing events** (logs, polling, file watching) → use `monitor`. The script keeps running and
|
|
5
|
+
- **Many ongoing events** (logs, polling, file watching) → use `monitor`. The script keeps running and new stdout is captured; persistent notifications are coalesced so ordinary log traffic does not create one model turn per line.
|
|
6
6
|
|
|
7
7
|
`monitor` uses the same permission rules as `bash`. To stop a monitor, cancel its background task via `job` with the returned `task_id`, or end the session.
|
|
8
8
|
|
|
@@ -15,7 +15,7 @@ Pick by how many notifications you need:
|
|
|
15
15
|
|
|
16
16
|
## Inputs
|
|
17
17
|
|
|
18
|
-
- `command` (required): shell command to run as a background monitor.
|
|
18
|
+
- `command` (required): shell command to run as a background monitor. Stdout is captured line-by-line; persistent notifications are coalesced before delivery.
|
|
19
19
|
- `kind` (required): one of `"log"`, `"poll"`, `"watch"`, `"other"`. Describes the monitoring strategy so listings can surface useful categories.
|
|
20
20
|
- `description` (required): short human-readable description of what is being monitored. Appears in task listings.
|
|
21
21
|
- `timeout` (optional): maximum wall-clock seconds the monitor may run before automatic shutdown. Omit for the session lifetime.
|
|
@@ -23,7 +23,7 @@ Pick by how many notifications you need:
|
|
|
23
23
|
|
|
24
24
|
## Output
|
|
25
25
|
|
|
26
|
-
Returns `Monitor started · task <task_id>` plus a task entry visible via `job({list: true})`.
|
|
26
|
+
Returns `Monitor started · task <task_id>` plus a task entry visible via `job({list: true})`. Persistent notifications contain the latest line and the count of earlier coalesced lines; terminal completion flushes the newest pending line.
|
|
27
27
|
|
|
28
28
|
## Cancellation
|
|
29
29
|
|
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
redactBrokerDiscovery,
|
|
25
25
|
} from "./discovery";
|
|
26
26
|
import { deriveIdempotencyIdentity } from "./identity";
|
|
27
|
-
import { executeLifecycle, isCanonicalSessionId } from "./lifecycle";
|
|
27
|
+
import { canonicalDeleteLocatorPath, executeLifecycle, isCanonicalSessionId } from "./lifecycle";
|
|
28
28
|
|
|
29
29
|
import {
|
|
30
30
|
type LifecycleDurableEffectsReceipt,
|
|
@@ -71,6 +71,7 @@ export type BrokerErrorCode =
|
|
|
71
71
|
export type BrokerCleanupIdentity = {
|
|
72
72
|
dev: string;
|
|
73
73
|
ino: string;
|
|
74
|
+
nlink?: string;
|
|
74
75
|
size: number;
|
|
75
76
|
mtimeNs: string;
|
|
76
77
|
sha256: string;
|
|
@@ -102,6 +103,7 @@ export type BrokerArtifactTree = {
|
|
|
102
103
|
|
|
103
104
|
export type BrokerCleanupEvidence = {
|
|
104
105
|
phase: "artifacts" | "transcript" | "metadata" | "lifecycle";
|
|
106
|
+
cleanupReceiptVersion?: 1;
|
|
105
107
|
/** Ledger-bound deletion target; never reconstructed from a retry request. */
|
|
106
108
|
sessionsRoot?: string;
|
|
107
109
|
transcriptPath?: string;
|
|
@@ -110,6 +112,7 @@ export type BrokerCleanupEvidence = {
|
|
|
110
112
|
sessionId?: string;
|
|
111
113
|
artifactsIdentity?: BrokerCleanupIdentity;
|
|
112
114
|
transcriptIdentity?: BrokerCleanupIdentity;
|
|
115
|
+
transcriptParentIdentity?: { dev: string; ino: string };
|
|
113
116
|
/** Identity-bound lifecycle metadata marker retained when exact cleanup is deferred. */
|
|
114
117
|
metadataIdentity?: BrokerCleanupIdentity;
|
|
115
118
|
metadataPath?: string;
|
|
@@ -122,9 +125,17 @@ export type BrokerCleanupEvidence = {
|
|
|
122
125
|
/** Append-only terminal proof for lifecycle metadata cleanup. */
|
|
123
126
|
metadataCompleted?: true;
|
|
124
127
|
detachedArtifactsPath?: string;
|
|
128
|
+
retainedArtifactsSuccessorPath?: string;
|
|
129
|
+
retainedArtifactsPlaceholderPath?: string;
|
|
130
|
+
retainedArtifactsUnknownPath?: string;
|
|
131
|
+
retainedArtifactsSideAuthority?: "none" | "retained";
|
|
125
132
|
detachedTranscriptPath?: string;
|
|
133
|
+
retainedTranscriptSuccessorPath?: string;
|
|
134
|
+
retainedTranscriptPlaceholderPath?: string;
|
|
135
|
+
retainedTranscriptUnknownPath?: string;
|
|
126
136
|
/** Durable proof that artifact cleanup completed before transcript mutation. */
|
|
127
137
|
artifactsRemoved?: boolean;
|
|
138
|
+
artifactsAbsentAtAuthorization?: true;
|
|
128
139
|
/** Preauthorized no-replace artifact quarantine path persisted before detach. */
|
|
129
140
|
plannedArtifactsPath?: string;
|
|
130
141
|
/** Identity-bound artifact tree authority persisted before broker detach and replayed exactly. */
|
|
@@ -133,6 +144,7 @@ export type BrokerCleanupEvidence = {
|
|
|
133
144
|
plannedTranscriptPath?: string;
|
|
134
145
|
/** Fully identity-bound startup-failure cleanup plan, persisted before any detach. */
|
|
135
146
|
lifecycleFiles?: BrokerLifecycleCleanupFile[];
|
|
147
|
+
lifecycleParentIdentity?: { dev: string; ino: string };
|
|
136
148
|
/** Delete metadata receipts authorize only the canonical marker/ready sibling pair. */
|
|
137
149
|
lifecycleDeleteMetadata?: true;
|
|
138
150
|
};
|
|
@@ -157,6 +169,14 @@ function isCleanupPending(response: BrokerResponse): boolean {
|
|
|
157
169
|
return !response.ok && response.error.code === "cleanup_pending" && response.error.cleanup !== undefined;
|
|
158
170
|
}
|
|
159
171
|
|
|
172
|
+
function cleanupFromResponse(response: unknown): BrokerCleanupEvidence | undefined {
|
|
173
|
+
return isBrokerResponse(response) && !response.ok ? response.error.cleanup : undefined;
|
|
174
|
+
}
|
|
175
|
+
function pendingCleanupSessionId(response: BrokerResponse): string | undefined {
|
|
176
|
+
if (response.ok || response.error.code !== "cleanup_pending") return undefined;
|
|
177
|
+
return typeof response.error.cleanup?.sessionId === "string" ? response.error.cleanup.sessionId : undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
160
180
|
function lifecycleResponseState(response: BrokerResponse): LifecycleState {
|
|
161
181
|
if (response.ok) return "terminal_ok";
|
|
162
182
|
if (isCleanupPending(response)) return "effect_started";
|
|
@@ -165,8 +185,8 @@ function lifecycleResponseState(response: BrokerResponse): LifecycleState {
|
|
|
165
185
|
|
|
166
186
|
type InputNormalization = { input: Record<string, unknown> } | BrokerResponse;
|
|
167
187
|
|
|
168
|
-
function isBrokerResponse(value:
|
|
169
|
-
return "ok" in value;
|
|
188
|
+
function isBrokerResponse(value: unknown): value is BrokerResponse {
|
|
189
|
+
return typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean";
|
|
170
190
|
}
|
|
171
191
|
|
|
172
192
|
function normalizeAliasedString(
|
|
@@ -224,11 +244,12 @@ function normalizeBrokerInput(operation: string, input: Record<string, unknown>)
|
|
|
224
244
|
typeof input.target === "object" && input.target !== null && !Array.isArray(input.target)
|
|
225
245
|
? (input.target as Record<string, unknown>)
|
|
226
246
|
: undefined;
|
|
247
|
+
const normalizeLifecycleDirectory = operation === "session.delete" ? canonicalDeleteLocatorPath : path.resolve;
|
|
227
248
|
const cwd = normalizeAliasedString(
|
|
228
249
|
{ cwd: input.cwd, path: input.path, targetPath: target?.path },
|
|
229
250
|
"cwd",
|
|
230
251
|
["path", "targetPath"],
|
|
231
|
-
|
|
252
|
+
normalizeLifecycleDirectory,
|
|
232
253
|
);
|
|
233
254
|
if (cwd.error) return error("invalid_input", cwd.error);
|
|
234
255
|
if (cwd.value !== undefined) {
|
|
@@ -239,7 +260,7 @@ function normalizeBrokerInput(operation: string, input: Record<string, unknown>)
|
|
|
239
260
|
{ stateRoot: input.stateRoot, targetStateRoot: target?.stateRoot },
|
|
240
261
|
"stateRoot",
|
|
241
262
|
["targetStateRoot"],
|
|
242
|
-
|
|
263
|
+
normalizeLifecycleDirectory,
|
|
243
264
|
);
|
|
244
265
|
if (stateRoot.error) return error("invalid_input", stateRoot.error);
|
|
245
266
|
if (stateRoot.value !== undefined && (!cwd.value || stateRoot.value !== path.join(cwd.value, ".gjc", "state")))
|
|
@@ -254,6 +275,11 @@ function normalizeBrokerInput(operation: string, input: Record<string, unknown>)
|
|
|
254
275
|
if (Object.keys(normalizedTarget).length > 0) normalized.target = normalizedTarget;
|
|
255
276
|
else delete normalized.target;
|
|
256
277
|
}
|
|
278
|
+
if (operation === "session.delete") {
|
|
279
|
+
const sessionPath = normalizeAliasedString(input, "sessionPath", [], canonicalDeleteLocatorPath);
|
|
280
|
+
if (sessionPath.error) return error("invalid_input", sessionPath.error);
|
|
281
|
+
if (sessionPath.value !== undefined) normalized.sessionPath = sessionPath.value;
|
|
282
|
+
}
|
|
257
283
|
return { input: normalized };
|
|
258
284
|
}
|
|
259
285
|
function canonicalJson(value: unknown): string {
|
|
@@ -798,7 +824,61 @@ export class Broker {
|
|
|
798
824
|
.update(canonicalJson(lifecycleTarget(operation, input)))
|
|
799
825
|
.digest("hex");
|
|
800
826
|
const identity = await deriveIdempotencyIdentity(this.settings.agentDir, operation, idempotencyKey, target);
|
|
801
|
-
|
|
827
|
+
let reconstructedDeleteCleanup: BrokerCleanupEvidence | undefined;
|
|
828
|
+
if (operation === "session.delete" && input.cwd === undefined && input.sessionPath === undefined) {
|
|
829
|
+
const entry = this.ledger.get(identity);
|
|
830
|
+
const cleanup = cleanupFromResponse(entry?.response) ?? cleanupFromResponse(entry?.unresolvedCleanupResponse);
|
|
831
|
+
reconstructedDeleteCleanup = cleanup;
|
|
832
|
+
const requestedSessionId = typeof input.sessionId === "string" ? input.sessionId : undefined;
|
|
833
|
+
if (!cleanup) {
|
|
834
|
+
if (requestedSessionId && this.ledger.hasUncertainCleanupForSession(requestedSessionId, identity))
|
|
835
|
+
return error(
|
|
836
|
+
"terminal_uncertain",
|
|
837
|
+
"Session cleanup authority is uncertain and cannot be deleted safely",
|
|
838
|
+
);
|
|
839
|
+
const pending = requestedSessionId
|
|
840
|
+
? this.ledger.findCleanupPendingBySessionId(requestedSessionId, identity)
|
|
841
|
+
: undefined;
|
|
842
|
+
if (pending) {
|
|
843
|
+
const pendingResponse = cleanupFromResponse(pending.response)
|
|
844
|
+
? pending.response
|
|
845
|
+
: pending.unresolvedCleanupResponse;
|
|
846
|
+
if (isBrokerResponse(pendingResponse)) return pendingResponse;
|
|
847
|
+
return error(
|
|
848
|
+
"terminal_uncertain",
|
|
849
|
+
"Session cleanup authority is pending under another lifecycle identity",
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
if (entry) {
|
|
853
|
+
if (isBrokerResponse(entry.response)) return entry.response;
|
|
854
|
+
return error("terminal_uncertain", "Existing session.delete ledger evidence lacks replayable authority");
|
|
855
|
+
}
|
|
856
|
+
if (requestedSessionId) {
|
|
857
|
+
await this.index.refresh();
|
|
858
|
+
if (this.index.listSessions().sessions.some(session => session.sessionId === requestedSessionId))
|
|
859
|
+
return error(
|
|
860
|
+
"terminal_uncertain",
|
|
861
|
+
"Indexed session requires durable locator authority before deletion",
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
return { ok: true, result: requestedSessionId ? { sessionId: requestedSessionId } : undefined };
|
|
865
|
+
}
|
|
866
|
+
if (
|
|
867
|
+
cleanup &&
|
|
868
|
+
cleanup.sessionId === requestedSessionId &&
|
|
869
|
+
typeof cleanup.cwd === "string" &&
|
|
870
|
+
typeof cleanup.transcriptPath === "string"
|
|
871
|
+
)
|
|
872
|
+
input = {
|
|
873
|
+
sessionId: cleanup.sessionId,
|
|
874
|
+
cwd: cleanup.cwd,
|
|
875
|
+
stateRoot: path.join(cleanup.cwd, ".gjc", "state"),
|
|
876
|
+
sessionPath: cleanup.transcriptPath,
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
const storedRequestHash = reconstructedDeleteCleanup ? this.ledger.get(identity)?.requestHash : undefined;
|
|
880
|
+
const requestHash =
|
|
881
|
+
storedRequestHash ?? createHash("sha256").update(canonicalJson({ operation, input })).digest("hex");
|
|
802
882
|
const prev = this.#chains.get(target) ?? Promise.resolve();
|
|
803
883
|
let release!: () => void;
|
|
804
884
|
const current = new Promise<void>(resolve => (release = resolve));
|
|
@@ -812,8 +892,8 @@ export class Broker {
|
|
|
812
892
|
const begun = await this.ledger.begin(identity, requestHash);
|
|
813
893
|
if (begun.kind === "replay") {
|
|
814
894
|
const replay = begun.entry.response as BrokerResponse;
|
|
815
|
-
|
|
816
|
-
|
|
895
|
+
const cleanup = cleanupFromResponse(replay) ?? reconstructedDeleteCleanup;
|
|
896
|
+
if (!cleanup) return replay;
|
|
817
897
|
const outcome = await executeLifecycle(this, operation, input, identity, cleanup);
|
|
818
898
|
const response = outcome.response;
|
|
819
899
|
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
@@ -828,11 +908,13 @@ export class Broker {
|
|
|
828
908
|
return error("idempotency_conflict", "idempotency key was used with a different request");
|
|
829
909
|
if (begun.kind === "terminal_uncertain") {
|
|
830
910
|
const replay = (begun.entry.response ?? beforeBegin?.response) as BrokerResponse | undefined;
|
|
831
|
-
|
|
911
|
+
const cleanup = (replay ? cleanupFromResponse(replay) : undefined) ?? reconstructedDeleteCleanup;
|
|
912
|
+
if (!cleanup)
|
|
832
913
|
return replay ?? error("terminal_uncertain", "prior lifecycle operation outcome is uncertain");
|
|
833
|
-
const outcome = await executeLifecycle(this, operation, input, identity,
|
|
914
|
+
const outcome = await executeLifecycle(this, operation, input, identity, cleanup);
|
|
834
915
|
const response = outcome.response;
|
|
835
916
|
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
917
|
+
...(pendingCleanupSessionId(response) ? { intendedSessionId: pendingCleanupSessionId(response) } : {}),
|
|
836
918
|
response,
|
|
837
919
|
responseDigest: createHash("sha256").update(canonicalJson(response)).digest("hex"),
|
|
838
920
|
...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
|
|
@@ -844,6 +926,7 @@ export class Broker {
|
|
|
844
926
|
const outcome = await executeLifecycle(this, operation, input, identity);
|
|
845
927
|
const response = outcome.response;
|
|
846
928
|
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
929
|
+
...(pendingCleanupSessionId(response) ? { intendedSessionId: pendingCleanupSessionId(response) } : {}),
|
|
847
930
|
resultSessionId:
|
|
848
931
|
response.ok && typeof (response.result as { sessionId?: unknown } | undefined)?.sessionId === "string"
|
|
849
932
|
? (response.result as { sessionId: string }).sessionId
|