@naswerks/periscope 1.0.0

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 (230) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +146 -0
  4. package/SECURITY.md +315 -0
  5. package/dist/bin/command.d.ts +97 -0
  6. package/dist/bin/command.js +141 -0
  7. package/dist/bin/config.d.ts +7 -0
  8. package/dist/bin/config.js +90 -0
  9. package/dist/bin/login.d.ts +48 -0
  10. package/dist/bin/login.js +82 -0
  11. package/dist/bin/main.d.ts +33 -0
  12. package/dist/bin/main.js +173 -0
  13. package/dist/bin/pair.d.ts +28 -0
  14. package/dist/bin/pair.js +198 -0
  15. package/dist/bin/periscope.d.ts +3 -0
  16. package/dist/bin/periscope.js +4 -0
  17. package/dist/bin/posture.d.ts +64 -0
  18. package/dist/bin/posture.js +157 -0
  19. package/dist/bin/reconfigure.d.ts +64 -0
  20. package/dist/bin/reconfigure.js +195 -0
  21. package/dist/bin/serve.d.ts +103 -0
  22. package/dist/bin/serve.js +441 -0
  23. package/dist/bin/status.d.ts +11 -0
  24. package/dist/bin/status.js +49 -0
  25. package/dist/bin/workspaces.d.ts +109 -0
  26. package/dist/bin/workspaces.js +215 -0
  27. package/dist/control/backoff.d.ts +25 -0
  28. package/dist/control/backoff.js +20 -0
  29. package/dist/control/codec.d.ts +15 -0
  30. package/dist/control/codec.js +501 -0
  31. package/dist/control/credential.d.ts +23 -0
  32. package/dist/control/credential.js +8 -0
  33. package/dist/control/frames.d.ts +1180 -0
  34. package/dist/control/frames.js +441 -0
  35. package/dist/control/link-state.d.ts +32 -0
  36. package/dist/control/link-state.js +39 -0
  37. package/dist/control/link.d.ts +109 -0
  38. package/dist/control/link.js +736 -0
  39. package/dist/control/queue.d.ts +110 -0
  40. package/dist/control/queue.js +174 -0
  41. package/dist/control/seq.d.ts +49 -0
  42. package/dist/control/seq.js +52 -0
  43. package/dist/control/stream-routing.d.ts +200 -0
  44. package/dist/control/stream-routing.js +167 -0
  45. package/dist/control/stream.d.ts +43 -0
  46. package/dist/control/stream.js +64 -0
  47. package/dist/core/async-queue.d.ts +26 -0
  48. package/dist/core/async-queue.js +63 -0
  49. package/dist/core/failure.d.ts +22 -0
  50. package/dist/core/failure.js +53 -0
  51. package/dist/core/index.d.ts +18 -0
  52. package/dist/core/index.js +7 -0
  53. package/dist/core/keyed-turns.d.ts +28 -0
  54. package/dist/core/keyed-turns.js +44 -0
  55. package/dist/core/paths.d.ts +25 -0
  56. package/dist/core/paths.js +77 -0
  57. package/dist/core/refusal.d.ts +43 -0
  58. package/dist/core/refusal.js +399 -0
  59. package/dist/core/result.d.ts +22 -0
  60. package/dist/core/result.js +16 -0
  61. package/dist/core/time.d.ts +19 -0
  62. package/dist/core/time.js +25 -0
  63. package/dist/core/vocab.d.ts +23 -0
  64. package/dist/core/vocab.js +37 -0
  65. package/dist/core/workspace-id.d.ts +12 -0
  66. package/dist/core/workspace-id.js +12 -0
  67. package/dist/gate/command.d.ts +136 -0
  68. package/dist/gate/command.js +551 -0
  69. package/dist/gate/decision.d.ts +111 -0
  70. package/dist/gate/decision.js +97 -0
  71. package/dist/gate/escalate.d.ts +82 -0
  72. package/dist/gate/escalate.js +92 -0
  73. package/dist/gate/gate.d.ts +173 -0
  74. package/dist/gate/gate.js +417 -0
  75. package/dist/gate/index.d.ts +39 -0
  76. package/dist/gate/index.js +31 -0
  77. package/dist/gate/jail.d.ts +92 -0
  78. package/dist/gate/jail.js +131 -0
  79. package/dist/gate/local.d.ts +69 -0
  80. package/dist/gate/local.js +51 -0
  81. package/dist/gate/outcome.d.ts +72 -0
  82. package/dist/gate/outcome.js +91 -0
  83. package/dist/gate/shell.d.ts +36 -0
  84. package/dist/gate/shell.js +226 -0
  85. package/dist/host/agent-process.d.ts +452 -0
  86. package/dist/host/agent-process.js +382 -0
  87. package/dist/host/bulk-post.d.ts +44 -0
  88. package/dist/host/bulk-post.js +114 -0
  89. package/dist/host/claude-transcripts.d.ts +84 -0
  90. package/dist/host/claude-transcripts.js +339 -0
  91. package/dist/host/config-file.d.ts +60 -0
  92. package/dist/host/config-file.js +216 -0
  93. package/dist/host/git-facts.d.ts +7 -0
  94. package/dist/host/git-facts.js +106 -0
  95. package/dist/host/hooks.d.ts +56 -0
  96. package/dist/host/hooks.js +75 -0
  97. package/dist/host/host.d.ts +356 -0
  98. package/dist/host/host.js +1294 -0
  99. package/dist/host/index.d.ts +35 -0
  100. package/dist/host/index.js +22 -0
  101. package/dist/host/link-state-file.d.ts +18 -0
  102. package/dist/host/link-state-file.js +66 -0
  103. package/dist/host/loopback.d.ts +38 -0
  104. package/dist/host/loopback.js +122 -0
  105. package/dist/host/machine.d.ts +9 -0
  106. package/dist/host/machine.js +19 -0
  107. package/dist/host/mcp-server.d.ts +11 -0
  108. package/dist/host/mcp-server.js +48 -0
  109. package/dist/host/package-facts.d.ts +3 -0
  110. package/dist/host/package-facts.js +26 -0
  111. package/dist/host/paired-credential-store.d.ts +32 -0
  112. package/dist/host/paired-credential-store.js +112 -0
  113. package/dist/host/paths.d.ts +80 -0
  114. package/dist/host/paths.js +165 -0
  115. package/dist/host/repository-read.d.ts +33 -0
  116. package/dist/host/repository-read.js +201 -0
  117. package/dist/host/session-store.d.ts +53 -0
  118. package/dist/host/session-store.js +100 -0
  119. package/dist/host/sign-in.d.ts +90 -0
  120. package/dist/host/sign-in.js +239 -0
  121. package/dist/host/telemetry.d.ts +42 -0
  122. package/dist/host/telemetry.js +74 -0
  123. package/dist/host/token-cache.d.ts +62 -0
  124. package/dist/host/token-cache.js +185 -0
  125. package/dist/host/transcript-fs.d.ts +4 -0
  126. package/dist/host/transcript-fs.js +104 -0
  127. package/dist/host/wire-request.d.ts +80 -0
  128. package/dist/host/wire-request.js +196 -0
  129. package/dist/host/workspace-fs.d.ts +14 -0
  130. package/dist/host/workspace-fs.js +84 -0
  131. package/dist/host/workspace-trust.d.ts +23 -0
  132. package/dist/host/workspace-trust.js +80 -0
  133. package/dist/identity/authorize.d.ts +59 -0
  134. package/dist/identity/authorize.js +72 -0
  135. package/dist/identity/config.d.ts +98 -0
  136. package/dist/identity/config.js +130 -0
  137. package/dist/identity/credential.d.ts +91 -0
  138. package/dist/identity/credential.js +76 -0
  139. package/dist/identity/device-code.d.ts +115 -0
  140. package/dist/identity/device-code.js +134 -0
  141. package/dist/identity/index.d.ts +33 -0
  142. package/dist/identity/index.js +10 -0
  143. package/dist/identity/mode.d.ts +106 -0
  144. package/dist/identity/mode.js +69 -0
  145. package/dist/identity/paired-credential.d.ts +52 -0
  146. package/dist/identity/paired-credential.js +43 -0
  147. package/dist/identity/pkce.d.ts +50 -0
  148. package/dist/identity/pkce.js +94 -0
  149. package/dist/identity/store.d.ts +43 -0
  150. package/dist/identity/store.js +43 -0
  151. package/dist/identity/token.d.ts +58 -0
  152. package/dist/identity/token.js +149 -0
  153. package/dist/index.d.ts +45 -0
  154. package/dist/index.js +53 -0
  155. package/dist/mcp/descriptor.d.ts +59 -0
  156. package/dist/mcp/descriptor.js +14 -0
  157. package/dist/mcp/index.d.ts +14 -0
  158. package/dist/mcp/index.js +4 -0
  159. package/dist/mcp/schema.d.ts +37 -0
  160. package/dist/mcp/schema.js +175 -0
  161. package/dist/mcp/server.d.ts +107 -0
  162. package/dist/mcp/server.js +66 -0
  163. package/dist/persistence/entry.d.ts +74 -0
  164. package/dist/persistence/entry.js +105 -0
  165. package/dist/persistence/index.d.ts +22 -0
  166. package/dist/persistence/index.js +8 -0
  167. package/dist/persistence/key.d.ts +46 -0
  168. package/dist/persistence/key.js +33 -0
  169. package/dist/persistence/mirror.d.ts +71 -0
  170. package/dist/persistence/mirror.js +57 -0
  171. package/dist/persistence/receipt.d.ts +111 -0
  172. package/dist/persistence/receipt.js +85 -0
  173. package/dist/persistence/retention.d.ts +68 -0
  174. package/dist/persistence/retention.js +68 -0
  175. package/dist/persistence/store.d.ts +106 -0
  176. package/dist/persistence/store.js +86 -0
  177. package/dist/persistence/transition-log.d.ts +73 -0
  178. package/dist/persistence/transition-log.js +133 -0
  179. package/dist/protocol.d.ts +27 -0
  180. package/dist/protocol.js +12 -0
  181. package/dist/sessions/index.d.ts +8 -0
  182. package/dist/sessions/index.js +4 -0
  183. package/dist/sessions/registry.d.ts +186 -0
  184. package/dist/sessions/registry.js +190 -0
  185. package/dist/sessions/session.d.ts +178 -0
  186. package/dist/sessions/session.js +288 -0
  187. package/dist/sessions/spawn-env.d.ts +88 -0
  188. package/dist/sessions/spawn-env.js +277 -0
  189. package/dist/state/coverage.d.ts +308 -0
  190. package/dist/state/coverage.js +315 -0
  191. package/dist/state/index.d.ts +12 -0
  192. package/dist/state/index.js +7 -0
  193. package/dist/state/machine.d.ts +112 -0
  194. package/dist/state/machine.js +237 -0
  195. package/dist/state/model.d.ts +233 -0
  196. package/dist/state/model.js +214 -0
  197. package/dist/state/observer.d.ts +55 -0
  198. package/dist/state/observer.js +413 -0
  199. package/dist/state/reporter.d.ts +45 -0
  200. package/dist/state/reporter.js +35 -0
  201. package/dist/state/store.d.ts +56 -0
  202. package/dist/state/store.js +120 -0
  203. package/dist/telemetry/index.d.ts +11 -0
  204. package/dist/telemetry/index.js +2 -0
  205. package/dist/telemetry/usage.d.ts +78 -0
  206. package/dist/telemetry/usage.js +69 -0
  207. package/dist/workspace/git-worktree.d.ts +150 -0
  208. package/dist/workspace/git-worktree.js +417 -0
  209. package/dist/workspace/index.d.ts +9 -0
  210. package/dist/workspace/index.js +4 -0
  211. package/dist/workspace/plain-dir.d.ts +34 -0
  212. package/dist/workspace/plain-dir.js +90 -0
  213. package/dist/workspace/provider.d.ts +152 -0
  214. package/dist/workspace/provider.js +2 -0
  215. package/dist/workspace/worktree-porcelain.d.ts +29 -0
  216. package/dist/workspace/worktree-porcelain.js +100 -0
  217. package/docs/architecture.md +277 -0
  218. package/docs/configuration.md +187 -0
  219. package/docs/gate.md +219 -0
  220. package/docs/identity.md +107 -0
  221. package/docs/protocol.md +501 -0
  222. package/docs/state-machine.md +160 -0
  223. package/examples/README.md +141 -0
  224. package/examples/minimal-controller/controller.ts +38 -0
  225. package/examples/parallel-run-proof/permission-mode-probe.ts +129 -0
  226. package/examples/parallel-run-proof/run.ts +850 -0
  227. package/examples/test-controller/controller.ts +655 -0
  228. package/examples/test-controller/serve.ts +29 -0
  229. package/examples/tsconfig.json +30 -0
  230. package/package.json +98 -0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * What this host can say about the sessions it is holding.
3
+ *
4
+ * This is not a roster, and the distinction is not pedantry. A roster spans every session
5
+ * everywhere — and sessions run on different hosts, because an agent runs where its host runs. No
6
+ * single host can produce one; it can only produce its own row. A controller aggregates these into
7
+ * the thing people actually want.
8
+ *
9
+ * So this offers enumeration and nothing else: no filtering, no search, no notion of which sessions
10
+ * are interesting, no "who is stuck". Every one of those is a judgement, and a host that makes
11
+ * judgements is a host that has to be rebuilt for the next product. If this file grows a predicate,
12
+ * the line has been crossed.
13
+ *
14
+ * What it does owe: the open-entry age. A session that has been in one tool call for forty
15
+ * minutes is the single most useful thing this layer can report, and the age is what says so.
16
+ */
17
+ import type { AgedEntry, SessionSnapshot } from './model.js';
18
+ import type { SessionStateMachine } from './machine.js';
19
+ export declare class SessionStateReporter {
20
+ #private;
21
+ /** Start reporting a session. Idempotent. */
22
+ add(machine: SessionStateMachine): void;
23
+ /**
24
+ * Stop reporting a session.
25
+ *
26
+ * A caller that removes an ended session loses its open-entry evidence from this view. That is
27
+ * the caller's decision to make and it is not made here: nothing self-evicts, because a session
28
+ * that ended holding work is exactly the one somebody will want to look at.
29
+ */
30
+ remove(machine: SessionStateMachine): void;
31
+ get count(): number;
32
+ /** Every session this host holds. Raw material — see this file's header. */
33
+ list(): SessionSnapshot[];
34
+ /**
35
+ * Every entry that has not exited, across this host's sessions, with its age and its session.
36
+ *
37
+ * Reported, never reconciled: an unpaired entry is surfaced with how long it has been open and
38
+ * whether it was marked abandoned, and nothing here closes one. Cleanup may mark; it may not
39
+ * erase, because erasing destroys the only evidence that the thing happened at all.
40
+ */
41
+ openEntries(): (AgedEntry & {
42
+ readonly sessionId: string | null;
43
+ })[];
44
+ }
45
+ //# sourceMappingURL=reporter.d.ts.map
@@ -0,0 +1,35 @@
1
+ export class SessionStateReporter {
2
+ #machines = new Set();
3
+ /** Start reporting a session. Idempotent. */
4
+ add(machine) {
5
+ this.#machines.add(machine);
6
+ }
7
+ /**
8
+ * Stop reporting a session.
9
+ *
10
+ * A caller that removes an ended session loses its open-entry evidence from this view. That is
11
+ * the caller's decision to make and it is not made here: nothing self-evicts, because a session
12
+ * that ended holding work is exactly the one somebody will want to look at.
13
+ */
14
+ remove(machine) {
15
+ this.#machines.delete(machine);
16
+ }
17
+ get count() {
18
+ return this.#machines.size;
19
+ }
20
+ /** Every session this host holds. Raw material — see this file's header. */
21
+ list() {
22
+ return [...this.#machines].map((machine) => machine.snapshot());
23
+ }
24
+ /**
25
+ * Every entry that has not exited, across this host's sessions, with its age and its session.
26
+ *
27
+ * Reported, never reconciled: an unpaired entry is surfaced with how long it has been open and
28
+ * whether it was marked abandoned, and nothing here closes one. Cleanup may mark; it may not
29
+ * erase, because erasing destroys the only evidence that the thing happened at all.
30
+ */
31
+ openEntries() {
32
+ return [...this.#machines].flatMap((machine) => machine.openEntries().map((entry) => ({ ...entry, sessionId: machine.sessionId })));
33
+ }
34
+ }
35
+ //# sourceMappingURL=reporter.js.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Where recorded transitions are kept, and where anyone asking "what happened?" reads from.
3
+ *
4
+ * Separate from the machine on purpose. A machine that both emitted and answered questions about
5
+ * itself would let a test prove emission by asking the thing that emitted — the assertion and the
6
+ * subject would be the same object. This subscribes, so "no transition without a cause" is checked
7
+ * against what was actually retained rather than against what a call site claims it passed.
8
+ *
9
+ * Bounded, in memory, and not durable. It holds a window per session so a long-running host does
10
+ * not grow without limit; durability of the transition log belongs to the persistence layer, and
11
+ * this deliberately does not pretend to be it. `dropped` counts what fell out of the window rather
12
+ * than letting a full ring look like a quiet one.
13
+ */
14
+ import type { SessionTransition } from './model.js';
15
+ import type { RejectedTransition, SessionStateMachine, Unsubscribe } from './machine.js';
16
+ export interface TransitionStoreOptions {
17
+ readonly windowPerSession?: number;
18
+ }
19
+ export declare class TransitionStore {
20
+ #private;
21
+ constructor(options?: TransitionStoreOptions);
22
+ /**
23
+ * Retain everything one machine records, and everything it refuses.
24
+ *
25
+ * Both halves matter: a store that kept only the successes would answer "were all transitions
26
+ * caused?" with a yes it earned by discarding the counter-examples.
27
+ */
28
+ attach(machine: SessionStateMachine): Unsubscribe;
29
+ /**
30
+ * One session's transitions, oldest first.
31
+ *
32
+ * Transitions recorded before the agent named itself are re-keyed when it does. They were
33
+ * genuinely recorded without an id — that window is real — but leaving them in a separate bucket
34
+ * would mean the trace of a session's own start could not be read by that session's id, which is
35
+ * the one thing a reader has.
36
+ */
37
+ forSession(sessionId: string): readonly SessionTransition[];
38
+ /** Transitions recorded while no session had reported itself yet, across every machine. */
39
+ unidentified(): readonly SessionTransition[];
40
+ /** Everything retained, across every session. The subject of the store-level assertions. */
41
+ all(): SessionTransition[];
42
+ sessionIds(): string[];
43
+ /** Every refused transition still held, with why. Never summarised into a count alone. */
44
+ rejected(): readonly RejectedTransition[];
45
+ /** How many transitions fell out of a window. A full ring must not read as a quiet one. */
46
+ get droppedCount(): number;
47
+ /**
48
+ * How many rejections fell out of their window.
49
+ *
50
+ * Counted separately from `droppedCount` because the two mean opposite things: transitions
51
+ * falling out is an ordinary busy session, rejections falling out means something is refusing
52
+ * faster than anyone is reading, and collapsing them into one number would hide that.
53
+ */
54
+ get droppedRejectionCount(): number;
55
+ }
56
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1,120 @@
1
+ /** Per session. A session that outruns this loses its oldest transitions, and says how many. */
2
+ const DEFAULT_WINDOW = 500;
3
+ /** Keyed while a session has no id yet — see SessionTransition.sessionId. */
4
+ const UNIDENTIFIED = '(unidentified)';
5
+ export class TransitionStore {
6
+ #window;
7
+ #bySession = new Map();
8
+ #rejected = [];
9
+ #dropped = 0;
10
+ #droppedRejections = 0;
11
+ constructor(options = {}) {
12
+ this.#window = options.windowPerSession ?? DEFAULT_WINDOW;
13
+ }
14
+ /**
15
+ * Retain everything one machine records, and everything it refuses.
16
+ *
17
+ * Both halves matter: a store that kept only the successes would answer "were all transitions
18
+ * caused?" with a yes it earned by discarding the counter-examples.
19
+ */
20
+ attach(machine) {
21
+ const dropTransitions = machine.onTransition((transition) => this.#retain(transition));
22
+ const dropRejections = machine.onRejected((rejected) => {
23
+ // Bounded on the same terms as the transitions, and for the same reason: a rejection arrives
24
+ // from whatever drives the machine, and once that is a wire path the rate is a stranger's to
25
+ // set. An uncapped array beside a capped ring would make the counter-examples the thing that
26
+ // grows without limit — the one collection nobody thinks to watch.
27
+ this.#rejected.push(rejected);
28
+ while (this.#rejected.length > this.#window) {
29
+ this.#rejected.shift();
30
+ this.#droppedRejections += 1;
31
+ }
32
+ });
33
+ return () => {
34
+ dropTransitions();
35
+ dropRejections();
36
+ };
37
+ }
38
+ /**
39
+ * One session's transitions, oldest first.
40
+ *
41
+ * Transitions recorded before the agent named itself are re-keyed when it does. They were
42
+ * genuinely recorded without an id — that window is real — but leaving them in a separate bucket
43
+ * would mean the trace of a session's own start could not be read by that session's id, which is
44
+ * the one thing a reader has.
45
+ */
46
+ forSession(sessionId) {
47
+ return this.#bySession.get(sessionId) ?? [];
48
+ }
49
+ /** Transitions recorded while no session had reported itself yet, across every machine. */
50
+ unidentified() {
51
+ return this.#bySession.get(UNIDENTIFIED) ?? [];
52
+ }
53
+ /** Everything retained, across every session. The subject of the store-level assertions. */
54
+ all() {
55
+ return [...this.#bySession.values()].flat();
56
+ }
57
+ sessionIds() {
58
+ return [...this.#bySession.keys()].filter((key) => key !== UNIDENTIFIED);
59
+ }
60
+ /** Every refused transition still held, with why. Never summarised into a count alone. */
61
+ rejected() {
62
+ return this.#rejected;
63
+ }
64
+ /** How many transitions fell out of a window. A full ring must not read as a quiet one. */
65
+ get droppedCount() {
66
+ return this.#dropped;
67
+ }
68
+ /**
69
+ * How many rejections fell out of their window.
70
+ *
71
+ * Counted separately from `droppedCount` because the two mean opposite things: transitions
72
+ * falling out is an ordinary busy session, rejections falling out means something is refusing
73
+ * faster than anyone is reading, and collapsing them into one number would hide that.
74
+ */
75
+ get droppedRejectionCount() {
76
+ return this.#droppedRejections;
77
+ }
78
+ #retain(transition) {
79
+ const key = transition.sessionId ?? UNIDENTIFIED;
80
+ this.#adopt(transition);
81
+ const kept = this.#bySession.get(key) ?? [];
82
+ kept.push(transition);
83
+ while (kept.length > this.#window) {
84
+ kept.shift();
85
+ this.#dropped += 1;
86
+ }
87
+ this.#bySession.set(key, kept);
88
+ }
89
+ /** Move this machine's earlier, id-less transitions under the id it has just reported. */
90
+ #adopt(transition) {
91
+ const id = transition.sessionId;
92
+ if (id === null)
93
+ return;
94
+ const orphans = this.#bySession.get(UNIDENTIFIED);
95
+ if (orphans === undefined)
96
+ return;
97
+ // Only this machine's own: an id-less transition belongs to whichever machine produced the
98
+ // seq series it sits in, and a machine's seq is dense from 1. Anything at or above this
99
+ // transition's own seq cannot be its predecessor.
100
+ const mine = orphans.filter((orphan) => orphan.seq < transition.seq && sameOrigin(orphan, transition));
101
+ if (mine.length === 0)
102
+ return;
103
+ this.#bySession.set(UNIDENTIFIED, orphans.filter((orphan) => !mine.includes(orphan)));
104
+ const kept = this.#bySession.get(id) ?? [];
105
+ this.#bySession.set(id, [...mine, ...kept]);
106
+ }
107
+ }
108
+ /**
109
+ * Whether two transitions came from the same machine, without either carrying a machine id.
110
+ *
111
+ * `where` is per-machine and fixed for its lifetime, and `correlationId` is whatever the controller
112
+ * supplied for that one session — so together they distinguish two concurrent id-less starts unless
113
+ * a controller starts two sessions in the same directory with no correlation id, in which case
114
+ * their early transitions are genuinely indistinguishable and both are adopted. That is a truthful
115
+ * outcome rather than a wrong one: nothing is lost, and nothing is invented to prevent it.
116
+ */
117
+ function sameOrigin(left, right) {
118
+ return left.correlationId === right.correlationId && left.where.cwd === right.where.cwd;
119
+ }
120
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Usage, cost and rate-limit facts — consumed from what the agent reports, never recomputed.
3
+ *
4
+ * There is no price table in this package and there must not be one. The agent reports a cost per
5
+ * model, with the provider and the id it priced against, so a mixed-model turn is correct without
6
+ * this host knowing any rate. A table would be wrong twice over and silently: stale the moment a
7
+ * price moves, and blind to a turn that used more than one model.
8
+ */
9
+ export type { ModelSpend, RateLimitStanding, SpendTotal, TurnSpend } from './usage.js';
10
+ export { NO_SPEND, foldSpend, spendReconciles } from './usage.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export { NO_SPEND, foldSpend, spendReconciles } from './usage.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,78 @@
1
+ /**
2
+ * What a turn cost — consumed from what the agent reports, never computed from a price table.
3
+ *
4
+ * The whole point is that nothing here multiplies tokens by a rate. A host that keeps a per-model
5
+ * price table and does the arithmetic itself has two failure modes, and both are silent: a price
6
+ * changes and every historical figure is quietly wrong, and a mixed-model turn is costed at one
7
+ * model's rate because the table is keyed by "the session's model". The agent already reports a
8
+ * per-model cost, with the provider and the canonical id it priced against. So this reads.
9
+ *
10
+ * How that is actually proven, because "it only reads" is easy to claim and easy to regress: the
11
+ * tests feed a cost no price table could ever produce and assert it comes out unchanged. A
12
+ * reimplementation that started computing would have to reproduce an arbitrary number to stay green.
13
+ *
14
+ * No SDK types here. This is the model the layer above works in; `host/telemetry.ts` lifts it off
15
+ * the agent's own result message, the same way `readInitFacts` lifts the version receipt.
16
+ */
17
+ /** What one model cost within a turn. Every number is reported, none derived. */
18
+ export interface ModelSpend {
19
+ /** The model string the agent was keyed by — provider-specific ids and aliases included. */
20
+ readonly model: string;
21
+ readonly costUsd: number;
22
+ readonly inputTokens: number;
23
+ readonly outputTokens: number;
24
+ readonly cacheReadInputTokens: number;
25
+ readonly cacheCreationInputTokens: number;
26
+ /** The window this model was serving with. Absent is null, never zero. */
27
+ readonly contextWindow: number | null;
28
+ /** The id the agent priced against, when it differs from `model`. */
29
+ readonly canonicalModel: string | null;
30
+ /** Which API served it, when reported. */
31
+ readonly provider: string | null;
32
+ }
33
+ /** One turn's spend. */
34
+ export interface TurnSpend {
35
+ /** The agent's own total. Not the sum of `byModel` — see `spendReconciles`. */
36
+ readonly totalCostUsd: number;
37
+ readonly byModel: readonly ModelSpend[];
38
+ }
39
+ /** Spend accumulated across turns. */
40
+ export interface SpendTotal {
41
+ readonly totalCostUsd: number;
42
+ readonly turnCount: number;
43
+ /** Per model, summed across every turn folded in. */
44
+ readonly byModel: readonly ModelSpend[];
45
+ }
46
+ export declare const NO_SPEND: SpendTotal;
47
+ /**
48
+ * Add one turn to a running total.
49
+ *
50
+ * Models are merged by their reported key, not by the canonical id. Two entries keyed differently
51
+ * that price against one canonical model are genuinely two things the agent distinguished — an alias
52
+ * and a provider-specific id can bill differently — and collapsing them here would throw away the
53
+ * distinction the report was making.
54
+ */
55
+ export declare function foldSpend(prev: SpendTotal, turn: TurnSpend): SpendTotal;
56
+ /**
57
+ * Whether a turn's per-model costs add up to the total the agent reported.
58
+ *
59
+ * It is an observation, not a validation, and must not become one. Both numbers come from the
60
+ * agent; a mismatch means the agent counted something the per-model breakdown does not itemise, and
61
+ * that is the agent's business rather than a defect this host should refuse over. The reason to
62
+ * expose it is that a caller charging money wants to know which of the two it is quoting, and
63
+ * whether they agree.
64
+ *
65
+ * @param toleranceUsd absolute, because these are floats and an exact comparison would fail on
66
+ * arithmetic that is otherwise perfectly correct.
67
+ */
68
+ export declare function spendReconciles(turn: TurnSpend, toleranceUsd?: number): boolean;
69
+ /** How a rate limit currently stands, as the agent reports it. */
70
+ export interface RateLimitStanding {
71
+ readonly status: 'allowed' | 'allowed_warning' | 'rejected';
72
+ /** Epoch seconds, as reported. Null when the agent did not say. */
73
+ readonly resetsAt: number | null;
74
+ readonly limitType: string | null;
75
+ /** 0–1 where reported, else null. Never defaulted to 0 — that would read as "plenty left". */
76
+ readonly utilization: number | null;
77
+ }
78
+ //# sourceMappingURL=usage.d.ts.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * What a turn cost — consumed from what the agent reports, never computed from a price table.
3
+ *
4
+ * The whole point is that nothing here multiplies tokens by a rate. A host that keeps a per-model
5
+ * price table and does the arithmetic itself has two failure modes, and both are silent: a price
6
+ * changes and every historical figure is quietly wrong, and a mixed-model turn is costed at one
7
+ * model's rate because the table is keyed by "the session's model". The agent already reports a
8
+ * per-model cost, with the provider and the canonical id it priced against. So this reads.
9
+ *
10
+ * How that is actually proven, because "it only reads" is easy to claim and easy to regress: the
11
+ * tests feed a cost no price table could ever produce and assert it comes out unchanged. A
12
+ * reimplementation that started computing would have to reproduce an arbitrary number to stay green.
13
+ *
14
+ * No SDK types here. This is the model the layer above works in; `host/telemetry.ts` lifts it off
15
+ * the agent's own result message, the same way `readInitFacts` lifts the version receipt.
16
+ */
17
+ export const NO_SPEND = { totalCostUsd: 0, turnCount: 0, byModel: [] };
18
+ /**
19
+ * Add one turn to a running total.
20
+ *
21
+ * Models are merged by their reported key, not by the canonical id. Two entries keyed differently
22
+ * that price against one canonical model are genuinely two things the agent distinguished — an alias
23
+ * and a provider-specific id can bill differently — and collapsing them here would throw away the
24
+ * distinction the report was making.
25
+ */
26
+ export function foldSpend(prev, turn) {
27
+ const byModel = new Map();
28
+ for (const spend of prev.byModel)
29
+ byModel.set(spend.model, spend);
30
+ for (const spend of turn.byModel) {
31
+ const existing = byModel.get(spend.model);
32
+ byModel.set(spend.model, existing === undefined
33
+ ? spend
34
+ : {
35
+ ...existing,
36
+ costUsd: existing.costUsd + spend.costUsd,
37
+ inputTokens: existing.inputTokens + spend.inputTokens,
38
+ outputTokens: existing.outputTokens + spend.outputTokens,
39
+ cacheReadInputTokens: existing.cacheReadInputTokens + spend.cacheReadInputTokens,
40
+ cacheCreationInputTokens: existing.cacheCreationInputTokens + spend.cacheCreationInputTokens,
41
+ // Last report wins: these describe the model, not the usage, and the newest is truest.
42
+ contextWindow: spend.contextWindow ?? existing.contextWindow,
43
+ canonicalModel: spend.canonicalModel ?? existing.canonicalModel,
44
+ provider: spend.provider ?? existing.provider,
45
+ });
46
+ }
47
+ return {
48
+ totalCostUsd: prev.totalCostUsd + turn.totalCostUsd,
49
+ turnCount: prev.turnCount + 1,
50
+ byModel: [...byModel.values()],
51
+ };
52
+ }
53
+ /**
54
+ * Whether a turn's per-model costs add up to the total the agent reported.
55
+ *
56
+ * It is an observation, not a validation, and must not become one. Both numbers come from the
57
+ * agent; a mismatch means the agent counted something the per-model breakdown does not itemise, and
58
+ * that is the agent's business rather than a defect this host should refuse over. The reason to
59
+ * expose it is that a caller charging money wants to know which of the two it is quoting, and
60
+ * whether they agree.
61
+ *
62
+ * @param toleranceUsd absolute, because these are floats and an exact comparison would fail on
63
+ * arithmetic that is otherwise perfectly correct.
64
+ */
65
+ export function spendReconciles(turn, toleranceUsd = 1e-9) {
66
+ const summed = turn.byModel.reduce((total, spend) => total + spend.costUsd, 0);
67
+ return Math.abs(summed - turn.totalCostUsd) <= toleranceUsd;
68
+ }
69
+ //# sourceMappingURL=usage.js.map
@@ -0,0 +1,150 @@
1
+ /**
2
+ * A linked git worktree per session.
3
+ *
4
+ * The one rule here has destroyed committed work before: never `git worktree add -B` over a branch
5
+ * that already exists. `-B` is create-or-reset — it hard-resets the branch to the base ref — so
6
+ * re-provisioning a directory that was lost (a cleanup ran, a volume remounted) silently discards
7
+ * every commit made in it. The directory comes back looking correct and the work is gone. Nothing
8
+ * errors, nothing warns, and the loss is only visible to someone who knew what the branch used to
9
+ * point at.
10
+ *
11
+ * The rule: if the branch exists, attach to it and let it keep its own tip. Only a branch being
12
+ * created for the first time takes the `-B` form.
13
+ *
14
+ * That rule is a pure function here, and that is the point of the file's shape. `worktreeAddArgs`
15
+ * takes four values and returns an argv; the invariant is a unit test with no git, no disk and no
16
+ * repository. The alternative is a rule that only a destroyed branch could ever have proven.
17
+ *
18
+ * No key formula lives here. A branch naming convention is the controller's own convention, and
19
+ * one baked in here would be this package deciding what a session means. The caller supplies the
20
+ * branch name and the base ref.
21
+ */
22
+ import type { Result } from '../core/result.js';
23
+ import { MAX_WORKSPACE_ID_LENGTH } from '../core/workspace-id.js';
24
+ import type { CommandEffects, ReleaseOptions, ReleaseReceipt, Workspace, WorkspaceEffects, WorkspaceInventory, WorkspaceProvider } from './provider.js';
25
+ /**
26
+ * The exact `git worktree add` argv, and the one decision that matters.
27
+ *
28
+ * `branchExists` decides between two different commands, not between two spellings of one.
29
+ * exists gives `worktree add <path> <branch>` attach: the branch keeps its own tip.
30
+ * new gives `worktree add -B <branch> <path> <baseRef>` create, off the base ref.
31
+ * Passing `-B` in the first case hard-resets the branch and discards its commits. See this file's
32
+ * header — that is not a hypothetical.
33
+ *
34
+ * Pure and total: no filesystem, no git, no I/O. Exported so the rule is testable directly rather
35
+ * than only through a provider that would need a repository to exercise.
36
+ */
37
+ export declare function worktreeAddArgs(path: string, branch: string, baseRef: string, branchExists: boolean): readonly string[];
38
+ export interface GitWorktreeProviderOptions {
39
+ /** The repository the worktrees are linked to. Absolute. */
40
+ readonly repositoryRoot: string;
41
+ /** Where worktree directories are created. Absolute. */
42
+ readonly workspaceRoot: string;
43
+ /**
44
+ * The branch a session's worktree sits on, derived from its id.
45
+ *
46
+ * Supplied, not derived here. A branch naming convention is the caller's own; see the file
47
+ * header. Defaults to `periscope/{sessionId}` only so the provider is usable without one.
48
+ */
49
+ readonly branchFor?: (sessionId: string) => string;
50
+ /** What a new branch is created from. Defaults to `HEAD`. */
51
+ readonly baseRef?: string;
52
+ readonly effects: WorkspaceEffects;
53
+ readonly commands: CommandEffects;
54
+ }
55
+ export declare class GitWorktreeProvider implements WorkspaceProvider {
56
+ #private;
57
+ /** The checkout this provider clones from — see `WorkspaceProvider.repositoryRoot`. */
58
+ get repositoryRoot(): string;
59
+ constructor(options: GitWorktreeProviderOptions);
60
+ /** Where this session's worktree is, whether or not it exists yet. */
61
+ pathFor(sessionId: string): string;
62
+ provision(sessionId: string): Promise<Result<Workspace>>;
63
+ /**
64
+ * Release the worktree.
65
+ *
66
+ * The branch is never deleted unless the caller names it in the ask. Removing a directory is
67
+ * reversible — the commits are still on the branch. Deleting a branch is not, so branch deletion
68
+ * is opt-in per call (`deleteBranch`), refused for an unmerged branch unless forced, and the
69
+ * merged check runs before anything is removed: a refusal never leaves a half-cleaned key. A
70
+ * deletion that fails after the directory went is a partial, stated on the receipt.
71
+ *
72
+ * The branch is the one the worktree is on (from `worktree list`), which may differ from the
73
+ * scheme's render if an agent checked out elsewhere; only when the directory is already gone is
74
+ * the scheme's render the name.
75
+ */
76
+ release(sessionId: string, options?: ReleaseOptions): Promise<Result<ReleaseReceipt | undefined>>;
77
+ /** The key of the directory at `path` when it sits directly under the workspace root, else null. */
78
+ keyForPath(path: string): string | null;
79
+ /**
80
+ * Every worktree under the workspace root, from disk, with what git knows about its branch.
81
+ *
82
+ * Three commands for the whole list, plus one `rev-list --count` per branch entry: `worktree list --porcelain` for the worktrees, `for-each-ref`
83
+ * for every branch tip's date, and `branch --merged <default>` for the merged set. Only worktrees
84
+ * CONTAINED BY the workspace root are reported: the repository itself and any checkout the
85
+ * operator keeps elsewhere are not this provider's to list, so no cleanup composed from this
86
+ * answer can name them. `merged` is null when the repository names no default branch.
87
+ */
88
+ inventory(): Promise<Result<WorkspaceInventory>>;
89
+ }
90
+ /**
91
+ * A session id that cannot safely become a path segment or a branch-name component.
92
+ *
93
+ * Refused rather than sanitized. Rewriting an id to make it safe means the directory no longer
94
+ * corresponds to the id the caller used, so two different sessions can collapse onto one workspace —
95
+ * which is the exact isolation property the plain-directory tests check, broken quietly by a
96
+ * helpful-looking fix.
97
+ *
98
+ * The screen is the union of two rule sets. The id becomes both a directory segment (win32/POSIX)
99
+ * and a git branch-name component (`periscope/{id}` by default); an id containing a character that
100
+ * only git refuses would pass a separators-only screen and fail inside git as an unnamed
101
+ * `workspace-provision-failed`. So the guard screens every character either rule set refuses — the
102
+ * `git check-ref-format` component rules together with the win32 segment rules — and the refusal
103
+ * names the offending character and its rule. Deliberately not screened: win32 reserved device
104
+ * names (CON, NUL, …) — a name-list, not a character class; those still die loudly at provision
105
+ * under the named refusal below.
106
+ */
107
+ declare function rejectUnusableId<T>(sessionId: string): Result<T> | null;
108
+ /**
109
+ * The refname/path-segment class check: the first way this id would be refused by
110
+ * `git check-ref-format` (as the component of a branch name) or by win32 (as a path segment), as a
111
+ * human sentence — or null for an id both rule sets accept. Pure and total, exported so the class
112
+ * is testable without a provider.
113
+ */
114
+ export declare function refnameOrPathIllegality(sessionId: string): string | null;
115
+ export { rejectUnusableId };
116
+ /**
117
+ * The same screen as `rejectUnusableId`, as a sentence a caller can attach to its own noun.
118
+ *
119
+ * The host validates `session_new.workspaceKey` before any workspace is claimed, and its refusal
120
+ * must name the field — "the session id …" would send a controller author to the wrong key. The
121
+ * union class itself stays single-sourced in `refnameOrPathIllegality`; the two structural checks
122
+ * are restated here because their sentences are the caller's to phrase. Keep the three checks in
123
+ * step with `rejectUnusableId` above — a key this accepts and that refuses (or the reverse) would
124
+ * mean the host's guard and the provider's disagree about the same id.
125
+ */
126
+ export declare function unusableKeyProblem(key: string): string | null;
127
+ /** The bound on a workspace id; declared in `core/` so the wire codec can enforce it too. */
128
+ export { MAX_WORKSPACE_ID_LENGTH };
129
+ /**
130
+ * A key safe to echo into a refusal detail: verbatim when short, truncated with its length named
131
+ * when not. Every screen above bounds accepted ids, but the refusal for an over-length id still
132
+ * has to describe the very string that broke the bound — this is the one place an unbounded input
133
+ * reaches a detail, so it is bounded here rather than at each caller's discretion.
134
+ */
135
+ export declare function keyPreview(id: string): string;
136
+ /** Newest tip first; entries with no tip date (detached, unknown) after every dated one; ties by key. */
137
+ export declare function byNewestTip(a: {
138
+ readonly key: string;
139
+ readonly lastCommitAt: string | null;
140
+ }, b: {
141
+ readonly key: string;
142
+ readonly lastCommitAt: string | null;
143
+ }): number;
144
+ /**
145
+ * The key of `path` when it is an absolute path to a directory DIRECTLY under `root` (normalised),
146
+ * else null: the root itself, a nested path, a relative path and anything elsewhere are all null,
147
+ * so a release addressed by path can only ever name what a provision could have made.
148
+ */
149
+ export declare function keyDirectlyUnder(path: string, root: string): string | null;
150
+ //# sourceMappingURL=git-worktree.d.ts.map