@lmctl-ai/lmctl 0.1.342 → 0.2.3

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 (182) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/README.md +11 -51
  3. package/data/price-aliases.json +1 -0
  4. package/data/price-data.json +1 -1
  5. package/dist/cli/batch.d.ts +2 -0
  6. package/dist/cli/busy-evidence.d.ts +27 -0
  7. package/dist/cli/chat-cycle.d.ts +59 -0
  8. package/dist/cli/chat-exec.d.ts +83 -0
  9. package/dist/cli/chat-policy.d.ts +22 -0
  10. package/dist/cli/chat.d.ts +23 -0
  11. package/dist/cli/code-resolver.d.ts +8 -0
  12. package/dist/cli/common.d.ts +54 -0
  13. package/dist/cli/context.d.ts +12 -0
  14. package/dist/cli/cost.d.ts +2 -0
  15. package/dist/cli/export.d.ts +2 -0
  16. package/dist/cli/index.d.ts +3 -0
  17. package/dist/cli/index.js +1062 -1400
  18. package/dist/cli/infra.d.ts +78 -0
  19. package/dist/cli/ls.d.ts +2 -0
  20. package/dist/cli/mailbox-cli.d.ts +6 -0
  21. package/dist/cli/managed.d.ts +62 -0
  22. package/dist/cli/mcp-install.d.ts +23 -0
  23. package/dist/cli/mcp.d.ts +2 -0
  24. package/dist/cli/paths.d.ts +50 -0
  25. package/dist/cli/plan-wizard.d.ts +42 -0
  26. package/dist/cli/plan.d.ts +20 -0
  27. package/dist/cli/queue-delivery.d.ts +107 -0
  28. package/dist/cli/ratelimit.d.ts +3 -0
  29. package/dist/cli/remote-fetch.d.ts +7 -0
  30. package/dist/cli/run-queries.d.ts +82 -0
  31. package/dist/cli/running.d.ts +5 -0
  32. package/dist/cli/schema.sql +20 -1
  33. package/dist/cli/script.d.ts +6 -0
  34. package/dist/cli/script.js +2097 -0
  35. package/dist/cli/self-identity.d.ts +25 -0
  36. package/dist/cli/session-idle.d.ts +118 -0
  37. package/dist/cli/status.d.ts +22 -0
  38. package/dist/cli/tail.d.ts +18 -0
  39. package/dist/cli/target-resolver.d.ts +139 -0
  40. package/dist/cli/team-status.d.ts +91 -0
  41. package/dist/cli/teamfile-compat.d.ts +71 -0
  42. package/dist/cli/teamfile-store.d.ts +18 -0
  43. package/dist/cli/teamfile-template.d.ts +40 -0
  44. package/dist/cli/template-env.d.ts +25 -0
  45. package/dist/cli/terminal-lock.d.ts +25 -0
  46. package/dist/cli/terminal.d.ts +23 -0
  47. package/dist/cli/time-filter.d.ts +10 -0
  48. package/dist/cli/update-notifier.d.ts +11 -0
  49. package/dist/cli/version-util.d.ts +2 -0
  50. package/dist/config/seed_templates.d.ts +1 -0
  51. package/dist/db/backfill-event-log.d.ts +26 -0
  52. package/dist/db/delegation.d.ts +37 -0
  53. package/dist/db/escalation_vocab.d.ts +45 -0
  54. package/dist/db/event-log.d.ts +40 -0
  55. package/dist/db/index.d.ts +1213 -0
  56. package/dist/db/inflight-acquire.d.ts +70 -0
  57. package/dist/db/invocation-event-projections.d.ts +65 -0
  58. package/dist/db/ledger-identity.test.d.ts +1 -0
  59. package/dist/db/live-event-projections.d.ts +92 -0
  60. package/dist/db/migrate.d.ts +51 -0
  61. package/dist/db/open-readonly.d.ts +2 -0
  62. package/dist/db/process-liveness.d.ts +37 -0
  63. package/dist/db/sqlite.d.ts +47 -0
  64. package/dist/db/sqlite.test.d.ts +1 -0
  65. package/dist/db/terminal-lock.d.ts +14 -0
  66. package/dist/db/tracked-invocation-reconcile.d.ts +52 -0
  67. package/dist/db/tracked-invocation.d.ts +116 -0
  68. package/dist/mcp/dispatch.d.ts +45 -0
  69. package/dist/mcp/mediation.d.ts +128 -0
  70. package/dist/mcp/path-warning.d.ts +7 -0
  71. package/dist/mcp/server.d.ts +63 -0
  72. package/dist/mcp/tools.d.ts +24 -0
  73. package/dist/providers/adapter.d.ts +265 -0
  74. package/dist/providers/adapters/agy-adapter.d.ts +17 -0
  75. package/dist/providers/adapters/claude-adapter.d.ts +20 -0
  76. package/dist/providers/adapters/codex-adapter.d.ts +22 -0
  77. package/dist/providers/adapters/copilot-adapter.d.ts +16 -0
  78. package/dist/providers/adapters/dsh-adapter.d.ts +89 -0
  79. package/dist/providers/adapters/gemini-adapter.d.ts +44 -0
  80. package/dist/providers/adapters/hermes-adapter.d.ts +71 -0
  81. package/dist/providers/adapters/index.d.ts +35 -0
  82. package/dist/providers/adapters/kimi-adapter.d.ts +16 -0
  83. package/dist/providers/adapters/lmplayer-adapter.d.ts +14 -0
  84. package/dist/providers/adapters/opencode-adapter.d.ts +11 -0
  85. package/dist/providers/adapters/pi-adapter.d.ts +17 -0
  86. package/dist/providers/adapters/print-turn.d.ts +29 -0
  87. package/dist/providers/adapters/qwen-adapter.d.ts +11 -0
  88. package/dist/providers/agy.d.ts +32 -0
  89. package/dist/providers/base-adapter.d.ts +107 -0
  90. package/dist/providers/base.d.ts +12 -0
  91. package/dist/providers/binary-check.d.ts +102 -0
  92. package/dist/providers/bundle.d.ts +118 -0
  93. package/dist/providers/claude-mock.d.ts +90 -0
  94. package/dist/providers/claude.d.ts +21 -0
  95. package/dist/providers/codex-config.d.ts +15 -0
  96. package/dist/providers/codex.d.ts +25 -0
  97. package/dist/providers/config/model-selection-lint.d.ts +45 -0
  98. package/dist/providers/config/team-config.d.ts +183 -0
  99. package/dist/providers/config/types.d.ts +146 -0
  100. package/dist/providers/config/user-config.d.ts +88 -0
  101. package/dist/providers/copilot.d.ts +21 -0
  102. package/dist/providers/debug.d.ts +128 -0
  103. package/dist/providers/dsh-sidecar.d.ts +65 -0
  104. package/dist/providers/dsh-zstd.d.ts +27 -0
  105. package/dist/providers/dsh.d.ts +37 -0
  106. package/dist/providers/frontmatter.d.ts +46 -0
  107. package/dist/providers/gemini-session-reader.d.ts +151 -0
  108. package/dist/providers/gemini.d.ts +39 -0
  109. package/dist/providers/hermes.d.ts +90 -0
  110. package/dist/providers/index.d.ts +421 -0
  111. package/dist/providers/kimi.d.ts +37 -0
  112. package/dist/providers/model-catalog.d.ts +141 -0
  113. package/dist/providers/one-shot-timeout.d.ts +1 -0
  114. package/dist/providers/opencode-config.d.ts +62 -0
  115. package/dist/providers/opencode.d.ts +40 -0
  116. package/dist/providers/parsers/acp.d.ts +47 -0
  117. package/dist/providers/parsers/codex-app-server.d.ts +60 -0
  118. package/dist/providers/parsers/stream-json.d.ts +57 -0
  119. package/dist/providers/passive-readers/agy.d.ts +155 -0
  120. package/dist/providers/passive-readers/copilot.d.ts +173 -0
  121. package/dist/providers/passive-readers/dsh.d.ts +34 -0
  122. package/dist/providers/passive-readers/kimi.d.ts +44 -0
  123. package/dist/providers/passive-readers/opencode.d.ts +89 -0
  124. package/dist/providers/passive-readers/qwen.d.ts +151 -0
  125. package/dist/providers/passive-readers/shared.d.ts +138 -0
  126. package/dist/providers/passive-session-reader.d.ts +188 -0
  127. package/dist/providers/path-utils.d.ts +1 -0
  128. package/dist/providers/pi.d.ts +32 -0
  129. package/dist/providers/pricing.d.ts +107 -0
  130. package/dist/providers/qwen.d.ts +22 -0
  131. package/dist/providers/ratelimit-readings.d.ts +32 -0
  132. package/dist/providers/registry.d.ts +31 -0
  133. package/dist/providers/resume.d.ts +43 -0
  134. package/dist/providers/seed-instruction.d.ts +3 -0
  135. package/dist/providers/self-session-env.d.ts +21 -0
  136. package/dist/providers/session-limit.d.ts +9 -0
  137. package/dist/providers/strip-ansi.d.ts +5 -0
  138. package/dist/providers/transport-errors.d.ts +94 -0
  139. package/dist/providers/transport-lifecycle.d.ts +63 -0
  140. package/dist/providers/transport-wrappers.d.ts +110 -0
  141. package/dist/providers/transports/acp-profiles/config-option-step.d.ts +24 -0
  142. package/dist/providers/transports/acp-profiles/copilot.d.ts +12 -0
  143. package/dist/providers/transports/acp-profiles/gemini.d.ts +22 -0
  144. package/dist/providers/transports/acp-profiles/hermes.d.ts +27 -0
  145. package/dist/providers/transports/acp-profiles/index.d.ts +19 -0
  146. package/dist/providers/transports/acp-profiles/kimi.d.ts +24 -0
  147. package/dist/providers/transports/acp-profiles/opencode.d.ts +26 -0
  148. package/dist/providers/transports/acp-profiles/types.d.ts +258 -0
  149. package/dist/providers/transports/acp-rpc.d.ts +18 -0
  150. package/dist/providers/transports/acp.d.ts +215 -0
  151. package/dist/providers/transports/codex-app-server.d.ts +190 -0
  152. package/dist/providers/transports/hold-open.d.ts +120 -0
  153. package/dist/providers/transports/manager.d.ts +21 -0
  154. package/dist/providers/transports/stream-json.d.ts +162 -0
  155. package/dist/providers/transports/types.d.ts +56 -0
  156. package/dist/providers/types.d.ts +154 -0
  157. package/dist/providers/yolo-contract.d.ts +131 -0
  158. package/dist/script/adapter.d.ts +101 -0
  159. package/dist/script/batch-script.d.ts +7 -0
  160. package/dist/script/index.d.ts +3 -0
  161. package/dist/script/lmscript.d.ts +550 -0
  162. package/dist/shared/policy.d.ts +74 -0
  163. package/dist/shared-lib/db.d.ts +25 -0
  164. package/dist/shared-lib/passive-session-reader.d.ts +2 -0
  165. package/dist/shared-lib/provider-parse.d.ts +6 -0
  166. package/dist/templates/basedir.d.ts +52 -0
  167. package/dist/uri/lmctl_uri.d.ts +38 -0
  168. package/durable-memory/model_selection.md +122 -25
  169. package/examples/README.md +103 -0
  170. package/examples/anonymous-panel.lms +3 -0
  171. package/examples/batch-prompts.txt +9 -0
  172. package/examples/batchscript.lms +67 -0
  173. package/examples/n8n/agent-lib.lms +56 -0
  174. package/examples/n8n/git-lib.lms +93 -0
  175. package/examples/n8n/mock-support.db +0 -0
  176. package/examples/n8n/multi-agent-review.lms +107 -0
  177. package/examples/n8n/news-digest.lms +159 -0
  178. package/examples/n8n/rag-search.lms +163 -0
  179. package/examples/n8n/support-agent.lms +88 -0
  180. package/examples/n8n/ticket-triage.lms +156 -0
  181. package/package.json +13 -17
  182. package/dist/cli/side_effect_classifier.json +0 -19
@@ -0,0 +1,70 @@
1
+ import type { AgentInflightInput, AgentInflightRow, Dal } from './index.js';
2
+ import type { TrackedInvocationRow } from './tracked-invocation.js';
3
+ import type { TerminalLockRecord } from './terminal-lock.js';
4
+ export type InflightReclaimReason = 'force' | 'stale' | 'dead-pid' | 'session-idle';
5
+ export type SharedInflightAcquireResult = {
6
+ acquired: true;
7
+ lease: AgentInflightInput;
8
+ leaseId: string;
9
+ previous: AgentInflightRow | null;
10
+ reclaimReason: InflightReclaimReason | null;
11
+ } | {
12
+ acquired: false;
13
+ current: AgentInflightRow;
14
+ };
15
+ export interface SharedInflightAcquireInput {
16
+ receiver_teamfile: string;
17
+ receiver_alias: string;
18
+ sender_teamfile: string;
19
+ sender_alias: string;
20
+ force?: boolean;
21
+ reclaimReason?: InflightReclaimReason;
22
+ ttlMs: number;
23
+ }
24
+ export declare function inflightHeartbeatMsFromEnv(env?: NodeJS.ProcessEnv): number;
25
+ export declare function startAgentInflightHeartbeat(touch: (heartbeatAt: string) => boolean | Promise<boolean>, options?: {
26
+ intervalMs?: number;
27
+ onError?: (error: unknown) => void;
28
+ }): NodeJS.Timeout;
29
+ export type ReceiverBusyState = {
30
+ busy: false;
31
+ reclaimable?: ReceiverReclaimableHolder;
32
+ } | {
33
+ busy: true;
34
+ reason: 'terminal-held';
35
+ source: 'terminal-lock';
36
+ terminalLock: TerminalLockRecord;
37
+ } | {
38
+ busy: true;
39
+ reason: 'terminal-held';
40
+ source: 'terminal-invocation';
41
+ terminalInvocation: ReceiverTerminalInvocation;
42
+ } | {
43
+ busy: true;
44
+ reason: 'in-flight';
45
+ inFlight: AgentInflightRow;
46
+ };
47
+ /**
48
+ * A holder row exists but is NOT live, so the receiver is correctly reported
49
+ * idle (the lock is reclaimable) — yet the orphaned row lingers until the next
50
+ * acquire lazily reclaims it. Surfaced (proposal 4, session-liveness
51
+ * observability) so `lmctl status` can show "held by pid N, appears
52
+ * dead" instead of an "idle" indistinguishable from "no lock at all". This does
53
+ * NOT change the busy verdict: a dead holder is, and stays, not-busy.
54
+ */
55
+ export interface ReceiverReclaimableHolder {
56
+ source: 'terminal-lock' | 'terminal-invocation' | 'in-flight';
57
+ holder_pid: number | null;
58
+ holder_host: string | null;
59
+ /** `dead-pid`: a local pid that is not alive. `stale`: a null pid or a
60
+ * remote/heartbeat-expired holder past its freshness window. */
61
+ reason: 'dead-pid' | 'stale';
62
+ }
63
+ export type ReceiverTerminalInvocation = Pick<TrackedInvocationRow, 'id' | 'started_at' | 'last_heartbeat_at' | 'holder_pid' | 'holder_host'>;
64
+ export type TrackedInvocationHolderLiveness = 'live' | 'dead' | 'unknown' | null;
65
+ export declare function acquireSharedInflight(dal: Pick<Dal, 'getAgentInflight' | 'tryAcquireAgentInflight'>, input: SharedInflightAcquireInput): SharedInflightAcquireResult;
66
+ export declare function isReceiverBusy(dal: Pick<Dal, 'getAgentInflight' | 'getTerminalLock' | 'query'>, receiverTeamfile: string, receiverAlias: string): boolean;
67
+ export declare function getTrackedInvocationHolderLiveness(row: Pick<TrackedInvocationRow, 'status' | 'holder_pid' | 'holder_host'>): TrackedInvocationHolderLiveness;
68
+ export declare function getReceiverBusyState(dal: Pick<Dal, 'getAgentInflight' | 'getTerminalLock' | 'query'>, receiverTeamfile: string, receiverAlias: string): ReceiverBusyState;
69
+ export declare function isReclaimableInflightHolder(row: AgentInflightRow, staleBefore: string): boolean;
70
+ export declare function inflightTtlMsFromEnv(env?: NodeJS.ProcessEnv): number;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Phase C dual-write for the INVOCATION lifecycle (Coder1's ownership).
3
+ *
4
+ * Mirror of Coder2's `live-event-projections.ts` for the mailbox/mutex side:
5
+ * each function appends the matching `message_event` row AND updates the
6
+ * `invocation_state` projection in the SAME transaction, routed through
7
+ * Architect's `appendEventWithProjection`. The caller (the DAL's
8
+ * tracked_invocation methods) wraps the legacy-table write and these appends in
9
+ * one `db.transaction`, so the old table and the new log/projection never land
10
+ * apart.
11
+ *
12
+ * TERMINAL TIER-2. A terminal-launched invocation additionally marks its target
13
+ * busy in `agent_state`. That table is Coder2's write-path end to end, so the
14
+ * terminal slice CALLS Coder2's `projectTerminalInvocation{Start,Finish}` —
15
+ * this module never writes `agent_state` fields directly. Receiver identity is
16
+ * `target_teamfile` + `target_alias` (never `from_identity`), per Coder2's
17
+ * busy-check contract.
18
+ *
19
+ * ROLLOUT SEAM — why FINISH/NOTIFY skip when the projection row is absent.
20
+ * Dual-write turns on at a deploy. Chat and terminal invocations start AND
21
+ * finish inside one same-binary process, so if the START was dual-written the
22
+ * FINISH will be too (row present) and vice versa — no seam. The one crossing
23
+ * case is the DAEMON reclaiming a stale invocation it did not start: that
24
+ * invocation may predate dual-write and have no `invocation_state` row. Coder2's
25
+ * `projectTerminalInvocationFinish` throws on a missing row, and a live DAL
26
+ * method must never throw on the seam. So FINISH/NOTIFY dual-write ONLY when the
27
+ * projection row exists (i.e. this invocation's START was dual-written); the
28
+ * pre-dual-write seam is reconciled by Phase D's cutover backfill, by design.
29
+ */
30
+ import type Database from './sqlite.js';
31
+ import type { TrackedInvocationRow } from './tracked-invocation.js';
32
+ /**
33
+ * INVOCATION_START — append the event and insert the `invocation_state` row.
34
+ * For a terminal-launched invocation with a target, also marks the target busy
35
+ * via Coder2's `agent_state` entry point. `row` is the freshly-inserted
36
+ * tracked_invocation row (so `id` and every field are populated).
37
+ */
38
+ export declare function appendInvocationStart(db: Database, row: TrackedInvocationRow): void;
39
+ /**
40
+ * INVOCATION_FINISH — append the event and move `invocation_state` to terminal.
41
+ * `row` is the tracked_invocation row AFTER the finish update. No-ops (returns
42
+ * false) when no projection row exists — the deploy-seam case reconciled by
43
+ * Phase D backfill (see module header).
44
+ */
45
+ export declare function appendInvocationFinish(db: Database, row: TrackedInvocationRow): boolean;
46
+ /**
47
+ * INVOCATION_NOTIFY_CLAIM — append the event and stamp the notifier onto
48
+ * `invocation_state`. No-ops on the deploy seam (no projection row).
49
+ */
50
+ export declare function appendInvocationNotifyClaim(db: Database, row: TrackedInvocationRow, notifiedBy: string, notifiedAt: string): boolean;
51
+ /**
52
+ * Heartbeat — a mutable projection touch, NOT a log event (design §3: heartbeats
53
+ * stay projection-only to avoid log bloat, and do not advance last_seq).
54
+ * Updates `invocation_state.last_heartbeat_at`, and for a terminal invocation
55
+ * the mirrored `agent_state.terminal_invocation_heartbeat_at` via Coder2's fn.
56
+ */
57
+ export declare function projectInvocationHeartbeat(db: Database, row: TrackedInvocationRow, heartbeatAt: string): void;
58
+ /** Mutable projection touch for a corrected request payload (no event; the
59
+ * verbatim-prompt correlation is Phase E's concern). */
60
+ export declare function projectInvocationRequestPayload(db: Database, invocationId: number, requestPayload: string): void;
61
+ /** Mutable projection touch persisting the CREATE-time request message linkage
62
+ * (`msg_tcl_<id>`) onto the running invocation, so the dead-holder reconciler can
63
+ * find and terminalize the exact message. No event: linkage metadata, like the
64
+ * request-payload touch above. */
65
+ export declare function projectInvocationRequestMessageId(db: Database, invocationId: number, requestMessageId: string): void;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,92 @@
1
+ import type Database from './sqlite.js';
2
+ import type { AgentInflightRow } from './delegation.js';
3
+ import type { TerminalLockRecord } from './terminal-lock.js';
4
+ export type InflightReclaimReason = 'force' | 'stale' | 'dead-pid' | 'session-idle';
5
+ export declare const CURRENT_HANDLING_TTL_MS: number;
6
+ export interface ReceiverEventAttribution {
7
+ provider?: string | null;
8
+ sessionId?: string | null;
9
+ }
10
+ export declare function teamChatEventStreamId(teamChatLogId: number): string;
11
+ export declare function invocationEventStreamId(invocationId: number): string;
12
+ /** Mutable per-turn pointer. Like last_heartbeat_at, it is deliberately not
13
+ * backed by its own message_event. */
14
+ export declare function projectCurrentlyHandlingMessage(db: Database, teamfile: string, alias: string, messageId: string, handlingSince?: string): void;
15
+ export declare function clearCurrentlyHandlingMessage(db: Database, teamfile: string, alias: string, expectedMessageId: string): boolean;
16
+ export interface SynchronousChatEventInput {
17
+ root_teamfile: string;
18
+ sender_teamfile: string | null;
19
+ sender_alias: string | null;
20
+ receiver_teamfile: string;
21
+ receiver_alias: string;
22
+ receiver_provider?: string | null;
23
+ receiver_session_id?: string | null;
24
+ provider_prompt?: string | null;
25
+ message: string;
26
+ response: string | null;
27
+ origin: string;
28
+ status: string;
29
+ error_detail: string | null;
30
+ truncated: number;
31
+ created_at: string;
32
+ }
33
+ /** Commit the live message identity before entering the provider turn. */
34
+ export declare function appendSynchronousChatCreated(db: Database, legacyChatLogId: number, input: SynchronousChatEventInput): string;
35
+ /** Append the terminal outcome and release only this turn's causal pointer. */
36
+ export declare function appendSynchronousChatHandled(db: Database, legacyChatLogId: number, input: SynchronousChatEventInput): string;
37
+ export declare function appendInflightAcquired(db: Database, current: AgentInflightRow, previous: AgentInflightRow | null, reclaimReason: InflightReclaimReason | null, callerStaleBefore?: string | null, force?: boolean): string;
38
+ /** Mutable in-flight heartbeat projection; deliberately no message_event row. */
39
+ export declare function projectInflightHeartbeat(db: Database, current: AgentInflightRow, leaseId: string, heartbeatAt: string): boolean;
40
+ export declare function appendInflightReleased(db: Database, current: AgentInflightRow, eventType: 'INFLIGHT_RELEASE' | 'INFLIGHT_RECLAIM', reason?: InflightReclaimReason): void;
41
+ export declare function appendTerminalLockTaken(db: Database, lock: TerminalLockRecord): void;
42
+ export declare function appendTerminalLockReleased(db: Database, lock: TerminalLockRecord, expired?: boolean): void;
43
+ export interface TerminalInvocationProjectionInput {
44
+ invocationId: string;
45
+ targetTeamfile: string;
46
+ targetAlias: string;
47
+ holderPid: number | null;
48
+ holderHost: string | null;
49
+ heartbeatAt: string;
50
+ }
51
+ /** Called from the INVOCATION_START append callback owned by the invocation path. */
52
+ export declare function projectTerminalInvocationStart(db: Database, eventSeq: number, input: TerminalInvocationProjectionInput): void;
53
+ /** Mutable heartbeat projection; deliberately no message_event heartbeat row. */
54
+ export declare function projectTerminalInvocationHeartbeat(db: Database, invocationId: string, heartbeatAt: string): boolean;
55
+ /** Called from the INVOCATION_FINISH append callback owned by the invocation path. */
56
+ export declare function projectTerminalInvocationFinish(db: Database, eventSeq: number, invocationId: string): void;
57
+ export interface MessageHandlingStartedResult {
58
+ appended: boolean;
59
+ seq: number;
60
+ handling_at: string;
61
+ }
62
+ export declare function appendMessageHandlingStarted(db: Database, opts: {
63
+ messageId: string;
64
+ handlingBy: string;
65
+ handlingTeamfile: string;
66
+ handlingAlias: string;
67
+ ts?: string;
68
+ }): MessageHandlingStartedResult;
69
+ export interface MessageAcknowledgedResult {
70
+ /** False when this actor already acked at the same stream state (retry). */
71
+ appended: boolean;
72
+ seq: number;
73
+ /** 'record-only' = terminalize was not requested; the receipt is recorded
74
+ * and normal delivery still happens later. 'removed' = this ack
75
+ * terminalized the queued pending row; 'claimed' = a delivery (daemon or
76
+ * sender-flush) already holds the message, removal no-oped and delivery
77
+ * proceeds; 'not-pending' = message already terminal. */
78
+ pending_outcome: 'record-only' | 'removed' | 'claimed' | 'not-pending';
79
+ }
80
+ export declare function appendMessageAcknowledged(db: Database, opts: {
81
+ messageId: string;
82
+ ackedBy: string;
83
+ note?: string;
84
+ ts?: string;
85
+ holderPid?: number;
86
+ holderHost?: string;
87
+ /** Opt-in (design.lmctl review): "an actor read this" and "the delivery
88
+ * obligation to the addressed receiver is fulfilled" are different facts.
89
+ * Default false = record-only; the relay script passes true explicitly
90
+ * because its read+ack composition IS the delivery-equivalent handling. */
91
+ terminalizePending?: boolean;
92
+ }): MessageAcknowledgedResult;
@@ -0,0 +1,51 @@
1
+ import Database from './sqlite.js';
2
+ export declare const CURRENT_USER_VERSION = 61;
3
+ export interface MigrateOptions {
4
+ dbPath: string;
5
+ /**
6
+ * Opt-in override for a db newer than this build understands (task #110
7
+ * ruling, 2026-09-18). Default `false`: refuse rather than silently
8
+ * degrade. lmctl's own house pattern is refuse-with-named-flag (9
9
+ * occurrences, e.g. `plan.ts`'s `--force`), not a blocking
10
+ * prompt — this codebase has NO user-stdin-prompt infrastructure at all
11
+ * (`isTTY` appears twice, both cosmetic), and the previous design's
12
+ * "no TTY -> proceed best-effort" escape hatch silently handed every
13
+ * headless agent the exact silent-degradation behavior this policy exists
14
+ * to abolish, on lmctl's majority (non-interactive) path.
15
+ */
16
+ allowSchemaSkew?: boolean;
17
+ /**
18
+ * Test-only seam: pretend this build's schema ceiling is this version
19
+ * instead of CURRENT_USER_VERSION, so the min_reader_version skew boundary
20
+ * can be exercised against a db this same code migrated forward. Only
21
+ * affects the skew comparison and its error/warning text; the fresh-create
22
+ * and migration paths always target CURRENT_USER_VERSION.
23
+ */
24
+ buildVersionForTests?: number;
25
+ }
26
+ /**
27
+ * A db schema newer than this build understands, encountered with
28
+ * `allowSchemaSkew` NOT set. Deliberately NOT a subtype of "this db file is
29
+ * broken" -- `createDal`'s recovery ladder (salvage + rebuild fresh) must
30
+ * never treat this as corruption; a live, valid, newer peer's real data
31
+ * would be destroyed. Callers that catch this must re-throw or handle it
32
+ * explicitly, never fall through to generic recovery.
33
+ */
34
+ export declare class SchemaSkewError extends Error {
35
+ readonly dbPath: string;
36
+ readonly dbVersion: number;
37
+ readonly buildVersion: number;
38
+ constructor(dbPath: string, dbVersion: number, buildVersion?: number);
39
+ }
40
+ export declare function migrate(opts: MigrateOptions): Database;
41
+ /** Test-only: clear the once-per-process warning latch between test cases. */
42
+ export declare function _resetNewerSchemaWarningForTests(): void;
43
+ export interface SchemaSkewInfo {
44
+ dbVersion: number;
45
+ buildVersion: number;
46
+ }
47
+ /** The current process's db-schema-skew state, if any was encountered by
48
+ * `migrate()` (proceed-allowed or refused) since the last reset. Read by
49
+ * the CLI's top-level error formatter to fold `schema_skew` into a
50
+ * `--json` refusal response. */
51
+ export declare function currentSchemaSkew(): SchemaSkewInfo | null;
@@ -0,0 +1,2 @@
1
+ import Database from './sqlite.js';
2
+ export declare function openStateDbReadOnly(dbPath: string): Database;
@@ -0,0 +1,37 @@
1
+ export declare function isProcessAlive(pid: number | null | undefined): boolean;
2
+ /**
3
+ * Process identity for records that outlive the process — the guard that keeps
4
+ * `serve stop` from SIGTERMing a bystander.
5
+ *
6
+ * TOKEN = `<boot_id>:<starttime>`.
7
+ *
8
+ * `/proc/<pid>/stat` field 22 (starttime) is ticks SINCE BOOT, so it only
9
+ * disambiguates reuse WITHIN one boot: after a reboot a fresh process can hold
10
+ * the same pid at the same tick and match a stale record exactly. Pairing it
11
+ * with `/proc/sys/kernel/random/boot_id` — regenerated every boot — closes
12
+ * that. Neither half is sufficient alone.
13
+ *
14
+ * Returns null when either half is unreadable (non-Linux, permissions, process
15
+ * gone). Null means UNVERIFIED, never "same".
16
+ *
17
+ * comm (field 2) is parenthesised and may contain spaces/parens, so fields are
18
+ * counted after the LAST ')'.
19
+ */
20
+ export declare function processStartToken(pid: number | null | undefined): string | null;
21
+ /** A token is only usable if it has the exact recorded shape. */
22
+ export declare function isWellFormedStartToken(token: unknown): token is string;
23
+ /**
24
+ * Tri-state identity verdict. Deliberately NOT a boolean: "we could not check"
25
+ * is a distinct outcome from "it is a different process", and collapsing them
26
+ * is what makes a signalling path unsafe.
27
+ *
28
+ * 'same' — alive AND identity confirmed.
29
+ * 'different' — alive but the pid was REUSED. Never signal.
30
+ * 'gone' — not alive.
31
+ * 'unverified' — alive, but identity could not be established (record
32
+ * predates tokens, malformed token, /proc unreadable,
33
+ * non-Linux). Callers that SIGNAL must refuse; callers that
34
+ * only report may say so.
35
+ */
36
+ export type ProcessIdentity = 'same' | 'different' | 'gone' | 'unverified';
37
+ export declare function verifyProcessIdentity(pid: number | null | undefined, expected: string | null | undefined): ProcessIdentity;
@@ -0,0 +1,47 @@
1
+ interface StatementSyncHandle {
2
+ all(...params: unknown[]): unknown[];
3
+ get(...params: unknown[]): unknown;
4
+ run(...params: unknown[]): {
5
+ changes: number | bigint;
6
+ lastInsertRowid: number | bigint;
7
+ };
8
+ setAllowBareNamedParameters(allowBareNamedParameters: boolean): void;
9
+ }
10
+ export interface DatabaseOptions {
11
+ readonly?: boolean;
12
+ fileMustExist?: boolean;
13
+ }
14
+ export interface RunResult {
15
+ changes: number;
16
+ lastInsertRowid: number;
17
+ }
18
+ declare class Database {
19
+ readonly name: string;
20
+ private readonly handle;
21
+ private transactionDepth;
22
+ private savepointId;
23
+ constructor(path: string, opts?: DatabaseOptions);
24
+ prepare(sql: string): Statement;
25
+ exec(sql: string): void;
26
+ pragma(str: string, opts?: {
27
+ simple?: boolean;
28
+ }): unknown;
29
+ transaction<Args extends unknown[], Return>(fn: (...args: Args) => Return): (...args: Args) => Return;
30
+ get inTransaction(): boolean;
31
+ close(): void;
32
+ private updateTransactionDepth;
33
+ }
34
+ export declare namespace Database {
35
+ type Database = InstanceType<typeof Database>;
36
+ }
37
+ export default Database;
38
+ export declare class Statement {
39
+ private readonly handle;
40
+ private rawMode;
41
+ constructor(handle: StatementSyncHandle);
42
+ get(...params: unknown[]): unknown;
43
+ all(...params: unknown[]): unknown[];
44
+ run(...params: unknown[]): RunResult;
45
+ raw(): this;
46
+ private normalizeRow;
47
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ export declare const TERMINAL_LOCK_SCOPE = "terminal_lock";
2
+ export declare const TERMINAL_LOCK_KIND = "member";
3
+ export interface TerminalLockRecord {
4
+ lock_id: string;
5
+ teamfile: string;
6
+ alias: string;
7
+ holder_pid: number;
8
+ holder_host: string;
9
+ holder_ips: string[];
10
+ started_at: string;
11
+ last_heartbeat: string;
12
+ }
13
+ export declare function terminalLockKey(teamfile: string, alias: string): string;
14
+ export declare function parseTerminalLock(data: string): TerminalLockRecord | null;
@@ -0,0 +1,52 @@
1
+ import type { Dal } from './index.js';
2
+ import type { TrackedInvocationRow } from './tracked-invocation.js';
3
+ export type TrackedInvocationReconcileRow = Pick<TrackedInvocationRow, 'id' | 'status' | 'holder_pid' | 'holder_host' | 'started_at' | 'last_heartbeat_at'>;
4
+ /**
5
+ * Shared trigger core (used by BOTH the on-touch and startup paths): if this
6
+ * running row's holder process is VERIFIED dead — same host as us AND its pid
7
+ * confirmed gone, i.e. `getTrackedInvocationHolderLiveness(row) === 'dead'` —
8
+ * transition it to the 'aborted' terminal state AND terminalize the linked chat
9
+ * message, atomically, via the guarded abort CAS.
10
+ *
11
+ * Returns the aborted row, or null when the holder is live, is a cross-host
12
+ * `'unknown'` (a verdict lmctl cannot actually prove — left untouched on
13
+ * purpose, matching the accepted PID-reuse-after-reboot boundary), is not
14
+ * running, or the compare-and-set lost a race. It NEVER aborts on anything
15
+ * short of a proven-dead local holder. The abort CAS is the single gate, so
16
+ * repeated sweeps and a late normal finalizer all no-op after the first winner
17
+ * (exactly one terminal message event).
18
+ */
19
+ export declare function abortTrackedInvocationIfHolderDead(dal: Pick<Dal, 'abortTrackedInvocationAndTerminalizeMessage'>, row: TrackedInvocationReconcileRow): TrackedInvocationRow | null;
20
+ /**
21
+ * Trigger 2 — daemon-startup sweep, for the OS-reboot case. Runs the SAME
22
+ * per-row liveness check as the on-touch trigger across every status='running'
23
+ * row exactly once at startup, aborting only the rows it verifies dead.
24
+ *
25
+ * This is deliberately NOT "the daemon just started, so presume everything is
26
+ * dead": `holder_pid` is the dispatching/holding CLI process's own pid
27
+ * (chat-exec.ts), wholly independent of the daemon's lifecycle, so a routine
28
+ * supervised daemon restart (ops/systemd `Restart=always`) MUST leave live
29
+ * holders alone. It is literally "run trigger 1's check, in bulk, once."
30
+ * Returns the number of rows aborted.
31
+ */
32
+ export declare function sweepDeadTrackedInvocations(dal: Pick<Dal, 'listAllRunningTrackedInvocations' | 'abortTrackedInvocationAndTerminalizeMessage'>): number;
33
+ /**
34
+ * Trigger 1 — opportunistic, on next access. Mirrors agent_inflight's
35
+ * reclaim-on-touch (`INFLIGHT_RECLAIM`): when a dispatch or the relay evaluates
36
+ * whether a receiver is busy, reconcile any running tracked_invocation rows
37
+ * targeting that receiver whose holder is verified dead, so a phantom 'running'
38
+ * does not linger until the next daemon restart. Same per-row check as the
39
+ * startup sweep. Returns the number of rows aborted.
40
+ */
41
+ export declare function reconcileDeadTrackedInvocationsForReceiver(dal: Pick<Dal, 'query' | 'abortTrackedInvocationAndTerminalizeMessage'>, receiverTeamfile: string, receiverAlias: string): number;
42
+ /**
43
+ * Trigger 3 (periodic daemon tick) -- claim every un-notified terminal
44
+ * tracked_invocation via the pre-existing `notified_by` CAS (orphaned since
45
+ * `lmctl wait`, its original consumer, was removed in the synchronous-
46
+ * delivery rework) and notify the ones that need it. The claim itself is the
47
+ * single "already handled" gate, so a concurrent or repeated sweep can never
48
+ * double-notify the same row. Returns the number of synthetic notifications
49
+ * created (not the number of rows claimed -- most claims are silent backlog
50
+ * drain of non-'aborted' terminal rows, see `notifyTrackedInvocationFailure`).
51
+ */
52
+ export declare function notifyTerminalTrackedInvocations(dal: Pick<Dal, 'listClaimableTerminalTrackedInvocations' | 'claimTrackedInvocationNotification' | 'insertTeamChatLog'>, notifiedBy: string, limit?: number): number;
@@ -0,0 +1,116 @@
1
+ import type Database from './sqlite.js';
2
+ export type TrackedInvocationKind = 'chat' | 'exec';
3
+ export type TrackedInvocationOrigin = 'agent' | 'operator' | 'mcp' | 'unknown';
4
+ export type TrackedInvocationLaunchSource = 'chat' | 'exec' | 'terminal' | 'unknown';
5
+ export type TrackedInvocationStatus = 'running' | 'done' | 'error' | 'busy' | 'cancelled' | 'aborted';
6
+ export interface TrackedInvocationRow {
7
+ id: number;
8
+ kind: TrackedInvocationKind;
9
+ origin: TrackedInvocationOrigin;
10
+ scope_teamfile: string | null;
11
+ from_identity: string;
12
+ target_teamfile: string | null;
13
+ target_alias: string | null;
14
+ root_teamfile: string | null;
15
+ cwd: string | null;
16
+ command: string | null;
17
+ status: TrackedInvocationStatus;
18
+ started_at: string;
19
+ ended_at: string | null;
20
+ holder_pid: number | null;
21
+ holder_host: string | null;
22
+ last_heartbeat_at: string | null;
23
+ exit_code: number | null;
24
+ signal: string | null;
25
+ result_summary: string | null;
26
+ error: string | null;
27
+ team_chat_log_id: number | null;
28
+ meta: string | null;
29
+ notified_by: string | null;
30
+ notified_at: string | null;
31
+ launch_source: TrackedInvocationLaunchSource | null;
32
+ request_payload: string | null;
33
+ /**
34
+ * The id of the tracked_invocation whose turn SENT the message that
35
+ * triggered this one -- the dispatch-lineage edge `lmctl running --tree`
36
+ * walks. Null for a root (a session not itself triggered by another
37
+ * tracked turn, e.g. the operator's own direct dispatch). Threaded via the
38
+ * LMCTL_INVOCATION_ID env var (set on the spawned provider process, read
39
+ * back by a NESTED dispatch it makes) -- not backfillable for rows
40
+ * created before this column existed.
41
+ */
42
+ parent_invocation_id: number | null;
43
+ /**
44
+ * JSON array of human-readable strings when this turn's provider silently
45
+ * dropped a declared model=/effort= (task #232) -- e.g.
46
+ * `["copilot terminal ignored model=X: <reason>"]`. Null when the
47
+ * provider honored its declared config; absence IS the signal, not a
48
+ * placeholder empty array.
49
+ */
50
+ config_warnings: string | null;
51
+ }
52
+ export interface TrackedInvocationInput {
53
+ kind: TrackedInvocationKind;
54
+ origin: TrackedInvocationOrigin;
55
+ /**
56
+ * Teamfile fields must already be canonical keys produced by the same
57
+ * resolver used for agent_inflight and team_chat_log. The DAL stores them
58
+ * verbatim so wait scopes cannot silently diverge from chat scopes.
59
+ */
60
+ scope_teamfile?: string | null;
61
+ from_identity: string;
62
+ target_teamfile?: string | null;
63
+ target_alias?: string | null;
64
+ root_teamfile?: string | null;
65
+ cwd?: string | null;
66
+ command?: string | null;
67
+ started_at?: string;
68
+ holder_pid?: number | null;
69
+ holder_host?: string | null;
70
+ last_heartbeat_at?: string | null;
71
+ meta?: string | null;
72
+ launch_source?: TrackedInvocationLaunchSource | null;
73
+ request_payload?: string | null;
74
+ parent_invocation_id?: number | null;
75
+ }
76
+ export interface TrackedInvocationFinishPatch {
77
+ status: Exclude<TrackedInvocationStatus, 'running'>;
78
+ ended_at?: string;
79
+ exit_code?: number | null;
80
+ signal?: string | null;
81
+ result_summary?: string | null;
82
+ error?: string | null;
83
+ team_chat_log_id?: number | null;
84
+ meta?: string | null;
85
+ /** JSON-encoded array of config-drop warning strings (task #232). Omit or
86
+ * pass null to leave any already-set value untouched (COALESCE, same
87
+ * convention as `meta`). */
88
+ config_warnings?: string | null;
89
+ }
90
+ export interface TrackedInvocationListFilter {
91
+ from?: string;
92
+ scope_teamfile?: string;
93
+ status?: TrackedInvocationStatus;
94
+ kind?: TrackedInvocationKind;
95
+ origin?: TrackedInvocationOrigin;
96
+ limit?: number;
97
+ }
98
+ export interface TrackedInvocationTerminalClaimFilter {
99
+ from?: string;
100
+ scope_teamfile?: string;
101
+ limit?: number;
102
+ }
103
+ export declare function prepareTrackedInvocationStatements(db: Database): {
104
+ insertTrackedInvocation: import("./sqlite.js").Statement;
105
+ getTrackedInvocation: import("./sqlite.js").Statement;
106
+ listAllRunningTrackedInvocations: import("./sqlite.js").Statement;
107
+ listRecentlyFinishedTrackedInvocations: import("./sqlite.js").Statement;
108
+ finishTrackedInvocation: import("./sqlite.js").Statement;
109
+ reclaimTrackedInvocationIfUnchanged: import("./sqlite.js").Statement;
110
+ heartbeatTrackedInvocation: import("./sqlite.js").Statement;
111
+ claimTrackedInvocationNotification: import("./sqlite.js").Statement;
112
+ updateTrackedInvocationRequestPayload: import("./sqlite.js").Statement;
113
+ linkTrackedInvocationChatLog: import("./sqlite.js").Statement;
114
+ abortTrackedInvocationIfUnchanged: import("./sqlite.js").Statement;
115
+ };
116
+ export declare function truncateTrackedInvocationField(value: string | null | undefined): string | null;
@@ -0,0 +1,45 @@
1
+ export interface BridgeConfig {
2
+ baseUrl: string;
3
+ token: string | null;
4
+ }
5
+ export interface BridgeOkResponse {
6
+ status: 'ok';
7
+ data: unknown;
8
+ }
9
+ export interface BridgeErrorResponse {
10
+ status: 'error';
11
+ code: number;
12
+ message: string;
13
+ body?: unknown;
14
+ }
15
+ export type BridgeResponse = BridgeOkResponse | BridgeErrorResponse;
16
+ /**
17
+ * Minimal fetch-shape injection point for tests. The default uses
18
+ * `globalThis.fetch` (Node 22 native).
19
+ */
20
+ export type FetchFn = (input: string, init: RequestInit) => Promise<Response>;
21
+ export interface DispatchDeps {
22
+ config: BridgeConfig;
23
+ fetchFn?: FetchFn;
24
+ dal?: unknown;
25
+ }
26
+ /**
27
+ * Validate tool name + input shape against the registered schema, dispatch,
28
+ * then wrap the response.
29
+ *
30
+ * Schema validation here is intentionally shallow (required-field +
31
+ * type-tag checks) — full JSON Schema validation would pull a dependency
32
+ * we don't need for Phase 1 and the apicli HTTP routes already validate
33
+ * payloads themselves.
34
+ */
35
+ export declare function dispatchTool(toolName: string, args: unknown, _deps: DispatchDeps): Promise<BridgeResponse>;
36
+ /**
37
+ * Read api.url + api.token from the canonical SQLite config table
38
+ * (DB > env > default for api.url; LMCTL_API_TOKEN > DB for api.token).
39
+ * The `lmctl api config` surface that used to write these was removed with
40
+ * the daemon surface (moved to lmbee) — operators set them via env vars or
41
+ * lmbee-side tooling.
42
+ */
43
+ export declare function readBridgeConfig(dal: {
44
+ getConfig(key: string): string | null;
45
+ }, env?: NodeJS.ProcessEnv): BridgeConfig;