@gajae-code/coding-agent 0.18.6 → 0.18.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/config/settings.d.ts +4 -0
  4. package/dist/types/discovery/helpers.d.ts +2 -2
  5. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  6. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  7. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  8. package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
  9. package/dist/types/sdk/host/host.d.ts +6 -0
  10. package/dist/types/sdk/router/session-router.d.ts +7 -0
  11. package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
  12. package/dist/types/session/internal/task-artifact-owner-access.d.ts +17 -0
  13. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +3 -0
  14. package/dist/types/session/session-retirement.d.ts +47 -0
  15. package/dist/types/session/session-storage.d.ts +0 -46
  16. package/dist/types/session/session-transcript-header.d.ts +2 -0
  17. package/dist/types/session/task-artifact-owner-codec.d.ts +107 -0
  18. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  19. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  20. package/package.json +7 -7
  21. package/scripts/resident-memory-bench.ts +174 -20
  22. package/src/capability/index.ts +1 -0
  23. package/src/capability/types.ts +4 -0
  24. package/src/cli/config-cli.ts +8 -18
  25. package/src/cli-main.ts +7 -8
  26. package/src/commands/sdk.ts +112 -21
  27. package/src/config/settings.ts +44 -4
  28. package/src/discovery/helpers.ts +9 -3
  29. package/src/discovery/ssh.ts +8 -3
  30. package/src/gjc-runtime/gc-runtime.ts +2 -1
  31. package/src/hindsight/config.ts +9 -3
  32. package/src/internal-urls/docs-index.generated.ts +2 -2
  33. package/src/modes/acp/acp-agent.ts +75 -3
  34. package/src/runtime-mcp/client.ts +13 -4
  35. package/src/runtime-mcp/elicitation.ts +142 -0
  36. package/src/sdk/broker/broker.ts +8 -1
  37. package/src/sdk/broker/lifecycle.ts +109 -7
  38. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  39. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  40. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  41. package/src/sdk/bus/index.ts +3 -3
  42. package/src/sdk/bus/telegram-daemon.ts +4 -2
  43. package/src/sdk/host/host.ts +6 -0
  44. package/src/sdk/host/session-runtime.test.ts +43 -2
  45. package/src/sdk/host/session-runtime.ts +2 -2
  46. package/src/sdk/mcp/server.ts +15 -3
  47. package/src/sdk/router/session-router.ts +19 -2
  48. package/src/sdk/session.ts +14 -0
  49. package/src/session/agent-session.ts +57 -6
  50. package/src/session/internal/managed-session-storage.ts +107 -1
  51. package/src/session/internal/task-artifact-owner-access.ts +229 -0
  52. package/src/session/internal/task-artifact-owner-transcript.ts +68 -0
  53. package/src/session/session-retirement.ts +155 -0
  54. package/src/session/session-storage.ts +1 -160
  55. package/src/session/session-transcript-header.ts +17 -0
  56. package/src/session/task-artifact-owner-codec.ts +578 -0
  57. package/src/session/task-artifact-owner-retirement.ts +475 -0
  58. package/src/session/task-artifact-owner.ts +289 -0
@@ -0,0 +1,155 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as path from "node:path";
3
+
4
+ import { toError } from "@gajae-code/utils";
5
+ import {
6
+ type FileSessionStorage,
7
+ SessionDeleteVerificationError,
8
+ type SessionStorageSnapshot,
9
+ type VerifiedSessionDeleteTarget,
10
+ } from "./session-storage";
11
+ import { parseFirstJsonlLine } from "./session-transcript-header";
12
+
13
+ /**
14
+ * Outcome of a disk-retention retirement attempt.
15
+ *
16
+ * `kept` means the transcript survived and nothing of the session was
17
+ * destroyed, with the exact reason so `gjc gc --disk` can report it.
18
+ * `cleanup_pending` means the delete authority already detached or removed the
19
+ * session's artifact tree (or quarantined the transcript) before it stopped:
20
+ * the record survives, the session does not, and a caller must NOT report that
21
+ * as an ordinary keep.
22
+ */
23
+ export type SessionRetirementOutcome =
24
+ | { kind: "retired" }
25
+ | { kind: "kept"; reason: string }
26
+ | { kind: "cleanup_pending"; reason: string };
27
+
28
+ /** A retirement that cleared every precondition the retention pass itself owns. */
29
+ type SessionRetirementPlan =
30
+ | { kind: "retirable"; target: VerifiedSessionDeleteTarget; artifactsPresent: boolean }
31
+ | { kind: "kept"; reason: string };
32
+
33
+ /**
34
+ * Evaluate the preconditions retirement owns — the transcript must be readable
35
+ * and carry a usable session header — and bind the delete target to the exact
36
+ * bytes just read. Nothing here mutates the store.
37
+ */
38
+ function planSessionRetirement(
39
+ storage: FileSessionStorage,
40
+ sessionsRoot: string,
41
+ transcriptPath: string,
42
+ ): SessionRetirementPlan {
43
+ let snapshot: SessionStorageSnapshot;
44
+ try {
45
+ snapshot = storage.readSnapshotSync(transcriptPath);
46
+ } catch (err) {
47
+ return { kind: "kept", reason: `transcript_unreadable: ${toError(err).message}` };
48
+ }
49
+
50
+ const header = parseFirstJsonlLine(snapshot.bytes);
51
+ if (header?.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string") {
52
+ return { kind: "kept", reason: "transcript_header_unusable" };
53
+ }
54
+
55
+ // The verified delete authority only unlinks a single-link transcript, so a
56
+ // hard-linked one can never be retired. Refusing here keeps the dry run from
57
+ // promising bytes prune cannot release, and — because this runs before the
58
+ // artifact phase — keeps a doomed retirement from destroying the artifact
59
+ // tree of a session whose transcript will survive anyway.
60
+ if (snapshot.stat.nlink === undefined || snapshot.stat.nlink !== 1n) {
61
+ return { kind: "kept", reason: "transcript_not_single_link" };
62
+ }
63
+
64
+ const directory = path.dirname(transcriptPath);
65
+ return {
66
+ kind: "retirable",
67
+ artifactsPresent: storage.existsSync(transcriptPath.slice(0, -".jsonl".length)),
68
+ target: {
69
+ sessionsRoot,
70
+ transcriptPath,
71
+ sessionId: header.id,
72
+ cwd: header.cwd,
73
+ transcriptIdentity: {
74
+ dev: snapshot.stat.dev,
75
+ ino: snapshot.stat.ino,
76
+ nlink: snapshot.stat.nlink,
77
+ size: snapshot.stat.size,
78
+ mtimeNs: snapshot.stat.mtimeNs,
79
+ sha256: crypto.createHash("sha256").update(snapshot.bytes).digest("hex"),
80
+ },
81
+ plannedArtifactsPath: path.join(directory, `.gjc-delete-gc-${crypto.randomUUID()}-artifacts`),
82
+ plannedTranscriptPath: path.join(directory, `.gjc-delete-gc-${crypto.randomUUID()}-transcript`),
83
+ },
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Non-mutating projection of {@link retireSessionTranscript}: would the
89
+ * retention pass's own preconditions let this transcript be retired at all?
90
+ *
91
+ * `gjc gc --disk` runs it so a dry run reports the verdict a prune would reach
92
+ * instead of promising bytes the delete authority will refuse to release. The
93
+ * authority's verdict (containment, identity, artifact tree) is deliberately
94
+ * not predicted here — it is re-derived against live state at delete time.
95
+ */
96
+ export function probeSessionRetirement(
97
+ storage: FileSessionStorage,
98
+ sessionsRoot: string,
99
+ transcriptPath: string,
100
+ ): { kind: "retirable" } | { kind: "kept"; reason: string } {
101
+ const plan = planSessionRetirement(storage, sessionsRoot, transcriptPath);
102
+ return plan.kind === "retirable" ? { kind: "retirable" } : plan;
103
+ }
104
+
105
+ /**
106
+ * A transcript that survives after its artifacts are already gone is not a
107
+ * benign keep: the session is half-destroyed and the caller has to surface it.
108
+ */
109
+ function retirementIncomplete(artifactsRemoved: boolean, reason: string): SessionRetirementOutcome {
110
+ return artifactsRemoved ? { kind: "cleanup_pending", reason } : { kind: "kept", reason };
111
+ }
112
+
113
+ /**
114
+ * Retire one managed session transcript through the verified hard-delete
115
+ * authority.
116
+ *
117
+ * This is the only supported way for a retention pass to remove a transcript:
118
+ * identity, containment, header id/cwd, artifact tree and parent directory are
119
+ * all re-verified inside {@link FileSessionStorage.deleteSessionVerified}, and
120
+ * anything ambiguous fails closed as `kept` rather than deleting bytes. The
121
+ * caller owns the retention policy; this function owns nothing but the delete
122
+ * authority.
123
+ */
124
+ export async function retireSessionTranscript(
125
+ storage: FileSessionStorage,
126
+ sessionsRoot: string,
127
+ transcriptPath: string,
128
+ ): Promise<SessionRetirementOutcome> {
129
+ const plan = planSessionRetirement(storage, sessionsRoot, transcriptPath);
130
+ if (plan.kind === "kept") return plan;
131
+
132
+ let artifactsRemoved = false;
133
+ try {
134
+ // Phase 1 removes the sibling artifact directory (or proves it absent);
135
+ // phase 2 unlinks the transcript only against the same bound identity.
136
+ const artifacts = await storage.deleteSessionVerified(plan.target);
137
+ if (artifacts.kind === "deleted") return { kind: "retired" };
138
+ if (artifacts.kind === "cleanup_pending") {
139
+ return { kind: "cleanup_pending", reason: `cleanup_pending_${artifacts.phase}: ${artifacts.error.message}` };
140
+ }
141
+ // Phase 1 is durable: whatever artifact tree the session had is gone, so
142
+ // every later refusal leaves a record without its artifacts.
143
+ artifactsRemoved = plan.artifactsPresent;
144
+ const deletion = await storage.deleteSessionVerified({ ...plan.target, artifactsRemoved: true });
145
+ if (deletion.kind === "deleted") return { kind: "retired" };
146
+ if (deletion.kind === "cleanup_pending") {
147
+ return { kind: "cleanup_pending", reason: `cleanup_pending_${deletion.phase}: ${deletion.error.message}` };
148
+ }
149
+ return retirementIncomplete(artifactsRemoved, "transcript_not_deleted");
150
+ } catch (err) {
151
+ const error = toError(err);
152
+ const kind = err instanceof SessionDeleteVerificationError ? err.kind : "error";
153
+ return retirementIncomplete(artifactsRemoved, `verified_delete_rejected(${kind}): ${error.message}`);
154
+ }
155
+ }
@@ -28,6 +28,7 @@ import {
28
28
  type NativePublishOutcome,
29
29
  } from "./internal/native-publish-outcome";
30
30
  import { isDerivedSessionMemoryFile } from "./internal/session-memory-sidecar";
31
+ import { parseFirstJsonlLine } from "./session-transcript-header";
31
32
 
32
33
  const utf8Decoder = new TextDecoder("utf-8");
33
34
  const newlineBuffer = Buffer.from("\n", "utf8");
@@ -2883,22 +2884,6 @@ export class FileSessionStorage implements SessionStorage {
2883
2884
  }
2884
2885
  }
2885
2886
 
2886
- /** Parse the first JSONL line as a generic record; returns undefined on parse failure. */
2887
- function parseFirstJsonlLine(bytes: Uint8Array): Record<string, unknown> | undefined {
2888
- const NL = 0x0a;
2889
- const end = bytes.indexOf(NL);
2890
- const firstLine = end === -1 ? bytes : bytes.subarray(0, end);
2891
- if (firstLine.length === 0) return undefined;
2892
- try {
2893
- const text = utf8Decoder.decode(firstLine).trim();
2894
- if (!text) return undefined;
2895
- const value: unknown = JSON.parse(text);
2896
- return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined;
2897
- } catch {
2898
- return undefined;
2899
- }
2900
- }
2901
-
2902
2887
  function matchesPattern(name: string, pattern: string): boolean {
2903
2888
  if (pattern === "*") return true;
2904
2889
  if (pattern === ".*") return name.startsWith(".");
@@ -3531,147 +3516,3 @@ export class MemorySessionStorage implements SessionStorage {
3531
3516
  return new MemoryStagedStreamingWriter(this, path);
3532
3517
  }
3533
3518
  }
3534
-
3535
- /**
3536
- * Outcome of a disk-retention retirement attempt.
3537
- *
3538
- * `kept` means the transcript survived and nothing of the session was
3539
- * destroyed, with the exact reason so `gjc gc --disk` can report it.
3540
- * `cleanup_pending` means the delete authority already detached or removed the
3541
- * session's artifact tree (or quarantined the transcript) before it stopped:
3542
- * the record survives, the session does not, and a caller must NOT report that
3543
- * as an ordinary keep.
3544
- */
3545
- export type SessionRetirementOutcome =
3546
- | { kind: "retired" }
3547
- | { kind: "kept"; reason: string }
3548
- | { kind: "cleanup_pending"; reason: string };
3549
-
3550
- /** A retirement that cleared every precondition the retention pass itself owns. */
3551
- type SessionRetirementPlan =
3552
- | { kind: "retirable"; target: VerifiedSessionDeleteTarget; artifactsPresent: boolean }
3553
- | { kind: "kept"; reason: string };
3554
-
3555
- /**
3556
- * Evaluate the preconditions retirement owns — the transcript must be readable
3557
- * and carry a usable session header — and bind the delete target to the exact
3558
- * bytes just read. Nothing here mutates the store.
3559
- */
3560
- function planSessionRetirement(
3561
- storage: FileSessionStorage,
3562
- sessionsRoot: string,
3563
- transcriptPath: string,
3564
- ): SessionRetirementPlan {
3565
- let snapshot: SessionStorageSnapshot;
3566
- try {
3567
- snapshot = storage.readSnapshotSync(transcriptPath);
3568
- } catch (err) {
3569
- return { kind: "kept", reason: `transcript_unreadable: ${toError(err).message}` };
3570
- }
3571
-
3572
- const header = parseFirstJsonlLine(snapshot.bytes);
3573
- if (header?.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string") {
3574
- return { kind: "kept", reason: "transcript_header_unusable" };
3575
- }
3576
-
3577
- // The verified delete authority only unlinks a single-link transcript, so a
3578
- // hard-linked one can never be retired. Refusing here keeps the dry run from
3579
- // promising bytes prune cannot release, and — because this runs before the
3580
- // artifact phase — keeps a doomed retirement from destroying the artifact
3581
- // tree of a session whose transcript will survive anyway.
3582
- if (snapshot.stat.nlink === undefined || snapshot.stat.nlink !== 1n) {
3583
- return { kind: "kept", reason: "transcript_not_single_link" };
3584
- }
3585
-
3586
- const directory = path.dirname(transcriptPath);
3587
- return {
3588
- kind: "retirable",
3589
- artifactsPresent: storage.existsSync(transcriptPath.slice(0, -".jsonl".length)),
3590
- target: {
3591
- sessionsRoot,
3592
- transcriptPath,
3593
- sessionId: header.id,
3594
- cwd: header.cwd,
3595
- transcriptIdentity: {
3596
- dev: snapshot.stat.dev,
3597
- ino: snapshot.stat.ino,
3598
- nlink: snapshot.stat.nlink,
3599
- size: snapshot.stat.size,
3600
- mtimeNs: snapshot.stat.mtimeNs,
3601
- sha256: createHash("sha256").update(snapshot.bytes).digest("hex"),
3602
- },
3603
- plannedArtifactsPath: path.join(directory, `.gjc-delete-gc-${randomUUID()}-artifacts`),
3604
- plannedTranscriptPath: path.join(directory, `.gjc-delete-gc-${randomUUID()}-transcript`),
3605
- },
3606
- };
3607
- }
3608
-
3609
- /**
3610
- * Non-mutating projection of {@link retireSessionTranscript}: would the
3611
- * retention pass's own preconditions let this transcript be retired at all?
3612
- *
3613
- * `gjc gc --disk` runs it so a dry run reports the verdict a prune would reach
3614
- * instead of promising bytes the delete authority will refuse to release. The
3615
- * authority's verdict (containment, identity, artifact tree) is deliberately
3616
- * not predicted here — it is re-derived against live state at delete time.
3617
- */
3618
- export function probeSessionRetirement(
3619
- storage: FileSessionStorage,
3620
- sessionsRoot: string,
3621
- transcriptPath: string,
3622
- ): { kind: "retirable" } | { kind: "kept"; reason: string } {
3623
- const plan = planSessionRetirement(storage, sessionsRoot, transcriptPath);
3624
- return plan.kind === "retirable" ? { kind: "retirable" } : plan;
3625
- }
3626
-
3627
- /**
3628
- * A transcript that survives after its artifacts are already gone is not a
3629
- * benign keep: the session is half-destroyed and the caller has to surface it.
3630
- */
3631
- function retirementIncomplete(artifactsRemoved: boolean, reason: string): SessionRetirementOutcome {
3632
- return artifactsRemoved ? { kind: "cleanup_pending", reason } : { kind: "kept", reason };
3633
- }
3634
-
3635
- /**
3636
- * Retire one managed session transcript through the verified hard-delete
3637
- * authority.
3638
- *
3639
- * This is the only supported way for a retention pass to remove a transcript:
3640
- * identity, containment, header id/cwd, artifact tree and parent directory are
3641
- * all re-verified inside {@link FileSessionStorage.deleteSessionVerified}, and
3642
- * anything ambiguous fails closed as `kept` rather than deleting bytes. The
3643
- * caller owns the retention policy; this function owns nothing but the delete
3644
- * authority.
3645
- */
3646
- export async function retireSessionTranscript(
3647
- storage: FileSessionStorage,
3648
- sessionsRoot: string,
3649
- transcriptPath: string,
3650
- ): Promise<SessionRetirementOutcome> {
3651
- const plan = planSessionRetirement(storage, sessionsRoot, transcriptPath);
3652
- if (plan.kind === "kept") return plan;
3653
-
3654
- let artifactsRemoved = false;
3655
- try {
3656
- // Phase 1 removes the sibling artifact directory (or proves it absent);
3657
- // phase 2 unlinks the transcript only against the same bound identity.
3658
- const artifacts = await storage.deleteSessionVerified(plan.target);
3659
- if (artifacts.kind === "deleted") return { kind: "retired" };
3660
- if (artifacts.kind === "cleanup_pending") {
3661
- return { kind: "cleanup_pending", reason: `cleanup_pending_${artifacts.phase}: ${artifacts.error.message}` };
3662
- }
3663
- // Phase 1 is durable: whatever artifact tree the session had is gone, so
3664
- // every later refusal leaves a record without its artifacts.
3665
- artifactsRemoved = plan.artifactsPresent;
3666
- const deletion = await storage.deleteSessionVerified({ ...plan.target, artifactsRemoved: true });
3667
- if (deletion.kind === "deleted") return { kind: "retired" };
3668
- if (deletion.kind === "cleanup_pending") {
3669
- return { kind: "cleanup_pending", reason: `cleanup_pending_${deletion.phase}: ${deletion.error.message}` };
3670
- }
3671
- return retirementIncomplete(artifactsRemoved, "transcript_not_deleted");
3672
- } catch (err) {
3673
- const error = toError(err);
3674
- const kind = err instanceof SessionDeleteVerificationError ? err.kind : "error";
3675
- return retirementIncomplete(artifactsRemoved, `verified_delete_rejected(${kind}): ${error.message}`);
3676
- }
3677
- }
@@ -0,0 +1,17 @@
1
+ const utf8Decoder = new TextDecoder("utf-8");
2
+
3
+ /** Parse the first JSONL line as a generic record; returns undefined on parse failure. */
4
+ export function parseFirstJsonlLine(bytes: Uint8Array): Record<string, unknown> | undefined {
5
+ const NL = 0x0a;
6
+ const end = bytes.indexOf(NL);
7
+ const firstLine = end === -1 ? bytes : bytes.subarray(0, end);
8
+ if (firstLine.length === 0) return undefined;
9
+ try {
10
+ const text = utf8Decoder.decode(firstLine).trim();
11
+ if (!text) return undefined;
12
+ const value: unknown = JSON.parse(text);
13
+ return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined;
14
+ } catch {
15
+ return undefined;
16
+ }
17
+ }