@sayknow-cli/coding-agent 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +157 -0
- package/dist/types/commands/session.d.ts +7 -0
- package/dist/types/config/telegram-autostart.d.ts +9 -1
- package/dist/types/modes/components/pet-capability.d.ts +8 -7
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/sayknow-pet-widget.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +1 -0
- package/dist/types/skc-runtime/boot-generation.d.ts +59 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +10 -2
- package/dist/types/skc-runtime/session-restore-runtime.d.ts +41 -0
- package/dist/types/skc-runtime/session-restore.d.ts +99 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +160 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +26 -1
- package/dist/types/tools/ask.d.ts +164 -4
- package/package.json +10 -7
- package/src/commands/session.ts +88 -2
- package/src/config/model-registry.ts +12 -0
- package/src/config/telegram-autostart.ts +11 -4
- package/src/defaults/skc/skills/deep-interview/SKILL.md +29 -3
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +1 -1
- package/src/modes/components/pet-capability.ts +22 -13
- package/src/modes/components/pet-selector.ts +1 -1
- package/src/modes/components/sayknow-pet-widget.ts +41 -7
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/shared/agent-wire/unattended-session.ts +40 -9
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -0
- package/src/notifications/lifecycle-control-runtime.ts +258 -179
- package/src/prompts/system/eager-todo.md +2 -0
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/prompts/system/system-prompt.md +4 -2
- package/src/sdk/bus/lifecycle-control-runtime.ts +189 -110
- package/src/session/agent-session.ts +31 -11
- package/src/skc-runtime/boot-generation.ts +172 -0
- package/src/skc-runtime/launch-tmux.ts +219 -41
- package/src/skc-runtime/session-restore-runtime.ts +120 -0
- package/src/skc-runtime/session-restore.ts +296 -0
- package/src/skc-runtime/session-state-sidecar.ts +41 -0
- package/src/skc-runtime/tmux-owner-isolation.ts +665 -0
- package/src/skc-runtime/tmux-sessions.ts +284 -108
- package/src/slash-commands/builtin-registry.ts +9 -4
- package/src/tools/ask.ts +183 -10
- package/src/tools/eval.ts +2 -2
|
@@ -302,6 +302,166 @@ export declare function replaceOwnerGenerationSync(stateDir: string, sessionId:
|
|
|
302
302
|
/** Publishes a raw-owner generation through the canonical SQLite-serialized CAS path. */
|
|
303
303
|
export declare function publishOwnerGenerationSync(request: PublishGenerationRequest): PublishGenerationResult;
|
|
304
304
|
export declare function createOwnerIntent(stateDir: string, input: Omit<OwnerIntent, "schema_version" | "intent_id" | "state">): Promise<OwnerIntent>;
|
|
305
|
+
/**
|
|
306
|
+
* Shared identity create fence (frozen contract F1'-F9''', architect-approved).
|
|
307
|
+
*
|
|
308
|
+
* Concurrent creators of the SAME canonical `(stateDir, sessionId)` identity must
|
|
309
|
+
* not both spawn a child: `SessionManager` opens transcripts with append flags and
|
|
310
|
+
* has no inter-process single-writer lock, so two owners on one transcript is a
|
|
311
|
+
* data-integrity failure rather than a recoverable skip.
|
|
312
|
+
*
|
|
313
|
+
* The fence deliberately does NOT hold the SQLite writer transaction across the
|
|
314
|
+
* spawn. `bootstrapTmuxOwnerIsolation()` runs in a separate process and acquires
|
|
315
|
+
* this very database twice itself, and the synchronous creator blocks on that
|
|
316
|
+
* helper through `Bun.spawnSync`, so a held transaction would deadlock the helper
|
|
317
|
+
* against its own parent. A long hold would also starve the 250 ms verdict path
|
|
318
|
+
* that shares this database. Instead each transition takes a short (target 50 ms)
|
|
319
|
+
* transaction that only reads and writes a durable reservation row; mutual
|
|
320
|
+
* exclusion outlives the transaction as row state, not as a held lock.
|
|
321
|
+
*/
|
|
322
|
+
export type IdentityCreatePhase = "reserved" | "helper_invoked" | "spawned" | "tagged" | "published";
|
|
323
|
+
/** Phases at or beyond which an untagged child may already exist on the server. */
|
|
324
|
+
export declare function identityCreatePhaseMayHaveChild(phase: IdentityCreatePhase): boolean;
|
|
325
|
+
export interface IdentityCreateKey {
|
|
326
|
+
stateDir: string;
|
|
327
|
+
sessionId: string;
|
|
328
|
+
stateFile: string;
|
|
329
|
+
}
|
|
330
|
+
export interface IdentityCreateReservation {
|
|
331
|
+
reservationId: string;
|
|
332
|
+
stateDir: string;
|
|
333
|
+
sessionId: string;
|
|
334
|
+
stateFile: string;
|
|
335
|
+
ownerPid: number;
|
|
336
|
+
ownerIncarnation: string;
|
|
337
|
+
phase: IdentityCreatePhase;
|
|
338
|
+
attemptSessionName: string | null;
|
|
339
|
+
nativeSessionId: string | null;
|
|
340
|
+
serverPid: number | null;
|
|
341
|
+
serverStartTime: string | null;
|
|
342
|
+
claimedAt: string;
|
|
343
|
+
updatedAt: string;
|
|
344
|
+
leaseDeadline: string;
|
|
345
|
+
}
|
|
346
|
+
export type IdentityCreateReservationResult = {
|
|
347
|
+
ok: true;
|
|
348
|
+
reservation: IdentityCreateReservation;
|
|
349
|
+
recovered: IdentityCreateReservation | null;
|
|
350
|
+
} | {
|
|
351
|
+
ok: false;
|
|
352
|
+
code: "identity_reserved_live" | "identity_reserved_unknown" | "identity_fence_contended" | "identity_incarnation_unavailable" | "identity_orphan_unresolved" | "identity_existing_owner";
|
|
353
|
+
existing: IdentityCreateReservation | null;
|
|
354
|
+
diagnostic: string;
|
|
355
|
+
};
|
|
356
|
+
/** Owner liveness is tri-state: only a proven-dead owner may be displaced. */
|
|
357
|
+
export type OwnerLiveness = "alive" | "dead" | "unknown";
|
|
358
|
+
/**
|
|
359
|
+
* `processIncarnation()` returns `undefined` both for a vanished process and for a
|
|
360
|
+
* failed probe, so existence is resolved first. Signal 0 distinguishes them:
|
|
361
|
+
* `ESRCH` proves absence, `EPERM` proves a live process owned by another user, and
|
|
362
|
+
* anything else is unknown. Only then does the incarnation discriminate PID reuse.
|
|
363
|
+
*/
|
|
364
|
+
export declare function probeOwnerLiveness(pid: number, recordedIncarnation: string, deps?: {
|
|
365
|
+
readIncarnation?: (pid: number) => string | undefined;
|
|
366
|
+
signal?: (pid: number) => void;
|
|
367
|
+
}): OwnerLiveness;
|
|
368
|
+
/** @internal Test seam for the incarnation reader. */
|
|
369
|
+
export declare function __setOwnerIncarnationReaderForTests(reader: ((pid: number) => string | undefined) | null): void;
|
|
370
|
+
export declare function canonicalIdentityCreateKey(key: IdentityCreateKey): IdentityCreateKey;
|
|
371
|
+
/**
|
|
372
|
+
* Ends the attempt while KEEPING the durable row.
|
|
373
|
+
*
|
|
374
|
+
* Used when cleanup after a spawn was uncertain: a child may survive that we
|
|
375
|
+
* could not remove, so the evidence must outlive the attempt. Unlike
|
|
376
|
+
* `releaseIdentityCreate` this leaves the row for authority-first recovery.
|
|
377
|
+
*/
|
|
378
|
+
export declare function abandonIdentityCreate(reservation: IdentityCreateReservation): void;
|
|
379
|
+
export interface ReserveIdentityCreateOptions {
|
|
380
|
+
ttlMs?: number;
|
|
381
|
+
ownerPid?: number;
|
|
382
|
+
ownerIncarnation?: string;
|
|
383
|
+
now?: () => Date;
|
|
384
|
+
probeLiveness?: (pid: number, incarnation: string) => OwnerLiveness;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Claims the identity when it is free, and otherwise reports why not.
|
|
388
|
+
*
|
|
389
|
+
* A dead previous owner is deliberately NOT displaced here: authority-first
|
|
390
|
+
* recovery has to census live tmux, which shells out and must never run inside
|
|
391
|
+
* the fence window. The caller performs that census and then calls
|
|
392
|
+
* {@link reclaimIdentityCreate}.
|
|
393
|
+
*
|
|
394
|
+
* The lease deadline is diagnostic only. Expiry never authorizes takeover; only a
|
|
395
|
+
* proven-dead owner does, because real creator paths block in unbounded
|
|
396
|
+
* `Bun.spawnSync` and cannot heartbeat while blocked.
|
|
397
|
+
*/
|
|
398
|
+
export declare function reserveIdentityCreate(rawKey: IdentityCreateKey, options?: ReserveIdentityCreateOptions): IdentityCreateReservationResult;
|
|
399
|
+
/**
|
|
400
|
+
* Completes authority-first recovery after the caller proved, outside the fence
|
|
401
|
+
* window, that the abandoned reservation left no authoritative child behind.
|
|
402
|
+
*
|
|
403
|
+
* `expectedReservationId` pins the exact abandoned row: if another process already
|
|
404
|
+
* recovered it, the row no longer matches and this fails closed rather than
|
|
405
|
+
* producing a second child.
|
|
406
|
+
*/
|
|
407
|
+
export declare function reclaimIdentityCreate(rawKey: IdentityCreateKey, expectedReservationId: string, options?: ReserveIdentityCreateOptions): IdentityCreateReservationResult;
|
|
408
|
+
/**
|
|
409
|
+
* What a live-tmux census concluded about the child an abandoned reservation may
|
|
410
|
+
* have left behind. `unknown` is not a soft failure: it is the fail-closed case.
|
|
411
|
+
*/
|
|
412
|
+
export type AbandonedIdentityVerdict = {
|
|
413
|
+
kind: "authoritative";
|
|
414
|
+
nativeSessionId: string;
|
|
415
|
+
} | {
|
|
416
|
+
kind: "orphan";
|
|
417
|
+
nativeSessionId: string;
|
|
418
|
+
} | {
|
|
419
|
+
kind: "absent";
|
|
420
|
+
} | {
|
|
421
|
+
kind: "unknown";
|
|
422
|
+
reason: string;
|
|
423
|
+
};
|
|
424
|
+
/**
|
|
425
|
+
* Injected so this module never imports the tmux session helpers (which already
|
|
426
|
+
* depend on it). The census shells out and therefore runs OUTSIDE the fence
|
|
427
|
+
* window, by construction.
|
|
428
|
+
*/
|
|
429
|
+
export interface AbandonedIdentityCensus {
|
|
430
|
+
inspect(evidence: {
|
|
431
|
+
stateDir: string;
|
|
432
|
+
sessionId: string;
|
|
433
|
+
attemptSessionName: string | null;
|
|
434
|
+
nativeSessionId: string | null;
|
|
435
|
+
}): AbandonedIdentityVerdict;
|
|
436
|
+
cleanupOrphan(nativeSessionId: string, attemptSessionName: string | null): void;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Authority-first recovery for a reservation whose owner is proven dead and that
|
|
440
|
+
* reached at least `helper_invoked`, so an untagged child may exist.
|
|
441
|
+
*
|
|
442
|
+
* The reservation's own `phase` is a hint, never the authority: a creator can
|
|
443
|
+
* publish its generation and die before recording `published`. The census reads
|
|
444
|
+
* the live canonical tags and the current published generation instead, so a
|
|
445
|
+
* valid child is preserved rather than killed — which is what keeps a
|
|
446
|
+
* tag-before-report crash from producing a successor.
|
|
447
|
+
*/
|
|
448
|
+
export declare function recoverAbandonedIdentityCreate(key: IdentityCreateKey, existing: IdentityCreateReservation, census: AbandonedIdentityCensus, options?: ReserveIdentityCreateOptions): IdentityCreateReservationResult;
|
|
449
|
+
export interface IdentityCreatePhasePatch {
|
|
450
|
+
attemptSessionName?: string | null;
|
|
451
|
+
nativeSessionId?: string | null;
|
|
452
|
+
serverPid?: number | null;
|
|
453
|
+
serverStartTime?: string | null;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Records a create-progress transition and renews the lease.
|
|
457
|
+
*
|
|
458
|
+
* Returns `null` when the reservation is no longer ours. The caller MUST treat
|
|
459
|
+
* that as a lost fence and perform no tmux mutation: another process has already
|
|
460
|
+
* recovered this identity.
|
|
461
|
+
*/
|
|
462
|
+
export declare function advanceIdentityCreatePhase(reservation: IdentityCreateReservation, phase: IdentityCreatePhase, patch?: IdentityCreatePhasePatch, options?: Pick<ReserveIdentityCreateOptions, "ttlMs" | "now">): IdentityCreateReservation | null;
|
|
463
|
+
/** Releases the reservation. Idempotent, and never deletes a successor's row. */
|
|
464
|
+
export declare function releaseIdentityCreate(reservation: IdentityCreateReservation): void;
|
|
305
465
|
/** Publish exactly one terminal verdict. Existing valid verdicts always win. */
|
|
306
466
|
export declare function observeOwnerTerminal(request: ObserveTerminalRequest): Promise<OwnerVerdict>;
|
|
307
467
|
/** Strictly validate the persisted authorization, optionally against its terminal observation. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type OwnerIsolationProbeSync, type PlanResponse, type TmuxOwnerIsolationExecutionDependencies, type TmuxOwnerIsolationExecutionResult } from "./tmux-owner-isolation";
|
|
1
|
+
import { type AbandonedIdentityCensus, type IdentityCreateKey, type IdentityCreateReservationResult, type OwnerIsolationProbeSync, type PlanResponse, type TmuxOwnerIsolationExecutionDependencies, type TmuxOwnerIsolationExecutionResult } from "./tmux-owner-isolation";
|
|
2
2
|
export interface SkcTmuxSessionStatus {
|
|
3
3
|
name: string;
|
|
4
4
|
attached: boolean;
|
|
@@ -67,6 +67,17 @@ export interface ForceCloseOwnerDependencies {
|
|
|
67
67
|
}
|
|
68
68
|
export interface CreateSkcTmuxSessionOptions {
|
|
69
69
|
platform?: NodeJS.Platform;
|
|
70
|
+
/**
|
|
71
|
+
* Restore only: run the child in the session's recorded directory instead of
|
|
72
|
+
* this process's cwd.
|
|
73
|
+
*/
|
|
74
|
+
cwd?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Restore only: argv appended to the `skc` child, e.g. `["--resume", "<id>"]`.
|
|
77
|
+
* Restore reuses this creator verbatim so it inherits the same identity fence
|
|
78
|
+
* and owner-isolation proof as an ordinary create.
|
|
79
|
+
*/
|
|
80
|
+
childArgs?: string[];
|
|
70
81
|
}
|
|
71
82
|
export type CreateOwnerIsolationTestDependencies = {
|
|
72
83
|
probe?: Partial<OwnerIsolationProbeSync>;
|
|
@@ -83,6 +94,20 @@ export declare function findSkcTmuxSessionByBranch(branch: string, env?: NodeJS.
|
|
|
83
94
|
export declare function findSkcTmuxSessionByName(sessionName: string, env?: NodeJS.ProcessEnv): SkcTmuxSessionStatus | undefined;
|
|
84
95
|
export declare function findSkcTmuxSessionByScope(project: string, branch: string | null | undefined, env?: NodeJS.ProcessEnv): SkcTmuxSessionStatus | undefined;
|
|
85
96
|
export declare function statusSkcTmuxSession(sessionName: string, env?: NodeJS.ProcessEnv): SkcTmuxSessionStatus;
|
|
97
|
+
/**
|
|
98
|
+
* Live-tmux census for authority-first recovery of an abandoned reservation.
|
|
99
|
+
*
|
|
100
|
+
* Authority is the CURRENT published generation on disk compared against the
|
|
101
|
+
* live session's canonical tags — never the reservation's own `phase`, because a
|
|
102
|
+
* creator can publish and then die before recording that it published.
|
|
103
|
+
*/
|
|
104
|
+
/**
|
|
105
|
+
* Resolves a reservation refused because its previous owner died after reaching
|
|
106
|
+
* the helper. Shared by every producer so none of them can brick an identity by
|
|
107
|
+
* treating an abandoned row as a permanent collision.
|
|
108
|
+
*/
|
|
109
|
+
export declare function settleAbandonedIdentity(reserved: IdentityCreateReservationResult, identityKey: IdentityCreateKey, census: AbandonedIdentityCensus): IdentityCreateReservationResult;
|
|
110
|
+
export declare function abandonedIdentityCensus(tmuxCommand: string, env: NodeJS.ProcessEnv, stateFile: string): AbandonedIdentityCensus;
|
|
86
111
|
export declare function createSkcTmuxSession(env?: NodeJS.ProcessEnv, options?: CreateSkcTmuxSessionOptions): SkcTmuxSessionStatus;
|
|
87
112
|
/** Proves a managed reusable name still resolves to one immutable session on one server. */
|
|
88
113
|
export declare function proveSkcTmuxSessionMutationTarget(sessionName: string, env?: NodeJS.ProcessEnv): ProvenTmuxSessionIdentity;
|
|
@@ -29,13 +29,92 @@ export declare const askSchema: z.ZodObject<{
|
|
|
29
29
|
}, z.core.$strip>>;
|
|
30
30
|
multi: z.ZodOptional<z.ZodBoolean>;
|
|
31
31
|
recommended: z.ZodOptional<z.ZodNumber>;
|
|
32
|
-
|
|
32
|
+
workflowGate: z.ZodOptional<z.ZodObject<{
|
|
33
|
+
stage: z.ZodEnum<{
|
|
34
|
+
"deep-interview": "deep-interview";
|
|
35
|
+
ralplan: "ralplan";
|
|
36
|
+
ultragoal: "ultragoal";
|
|
37
|
+
}>;
|
|
38
|
+
kind: z.ZodEnum<{
|
|
39
|
+
question: "question";
|
|
40
|
+
approval: "approval";
|
|
41
|
+
execution: "execution";
|
|
42
|
+
}>;
|
|
43
|
+
}, z.core.$strip>>;
|
|
44
|
+
deepInterview: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
45
|
+
round: z.ZodLiteral<0>;
|
|
46
|
+
component: z.ZodLiteral<"review-topology">;
|
|
47
|
+
intent_contract: z.ZodObject<{
|
|
48
|
+
items: z.ZodArray<z.ZodObject<{
|
|
49
|
+
id: z.ZodString;
|
|
50
|
+
category: z.ZodEnum<{
|
|
51
|
+
surface: "surface";
|
|
52
|
+
artifact: "artifact";
|
|
53
|
+
integration: "integration";
|
|
54
|
+
constraint: "constraint";
|
|
55
|
+
}>;
|
|
56
|
+
statement: z.ZodString;
|
|
57
|
+
}, z.core.$strict>>;
|
|
58
|
+
confirmation_options: z.ZodArray<z.ZodString>;
|
|
59
|
+
}, z.core.$strict>;
|
|
33
60
|
round_id: z.ZodOptional<z.ZodString>;
|
|
61
|
+
dimension: z.ZodString;
|
|
62
|
+
ambiguity: z.ZodNumber;
|
|
63
|
+
confused_terms: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
64
|
+
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
65
|
+
reference_id: z.ZodString;
|
|
66
|
+
label: z.ZodString;
|
|
67
|
+
origin: z.ZodString;
|
|
68
|
+
url: z.ZodOptional<z.ZodString>;
|
|
69
|
+
excerpt: z.ZodOptional<z.ZodString>;
|
|
70
|
+
}, z.core.$strict>>>;
|
|
71
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
34
72
|
round: z.ZodNumber;
|
|
73
|
+
intent_review: z.ZodObject<{
|
|
74
|
+
observed_items: z.ZodArray<z.ZodObject<{
|
|
75
|
+
id: z.ZodString;
|
|
76
|
+
category: z.ZodEnum<{
|
|
77
|
+
surface: "surface";
|
|
78
|
+
artifact: "artifact";
|
|
79
|
+
integration: "integration";
|
|
80
|
+
constraint: "constraint";
|
|
81
|
+
}>;
|
|
82
|
+
statement: z.ZodString;
|
|
83
|
+
}, z.core.$strict>>;
|
|
84
|
+
supporting_substitutions: z.ZodArray<z.ZodObject<{
|
|
85
|
+
removed_id: z.ZodString;
|
|
86
|
+
replacement_ids: z.ZodArray<z.ZodString>;
|
|
87
|
+
rationale: z.ZodString;
|
|
88
|
+
}, z.core.$strict>>;
|
|
89
|
+
approval_options: z.ZodArray<z.ZodString>;
|
|
90
|
+
}, z.core.$strict>;
|
|
91
|
+
round_id: z.ZodOptional<z.ZodString>;
|
|
35
92
|
component: z.ZodString;
|
|
36
93
|
dimension: z.ZodString;
|
|
37
94
|
ambiguity: z.ZodNumber;
|
|
38
|
-
|
|
95
|
+
confused_terms: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
96
|
+
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
97
|
+
reference_id: z.ZodString;
|
|
98
|
+
label: z.ZodString;
|
|
99
|
+
origin: z.ZodString;
|
|
100
|
+
url: z.ZodOptional<z.ZodString>;
|
|
101
|
+
excerpt: z.ZodOptional<z.ZodString>;
|
|
102
|
+
}, z.core.$strict>>>;
|
|
103
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
104
|
+
round_id: z.ZodOptional<z.ZodString>;
|
|
105
|
+
round: z.ZodNumber;
|
|
106
|
+
component: z.ZodString;
|
|
107
|
+
dimension: z.ZodString;
|
|
108
|
+
ambiguity: z.ZodNumber;
|
|
109
|
+
confused_terms: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
110
|
+
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
111
|
+
reference_id: z.ZodString;
|
|
112
|
+
label: z.ZodString;
|
|
113
|
+
origin: z.ZodString;
|
|
114
|
+
url: z.ZodOptional<z.ZodString>;
|
|
115
|
+
excerpt: z.ZodOptional<z.ZodString>;
|
|
116
|
+
}, z.core.$strict>>>;
|
|
117
|
+
}, z.core.$strict>]>>;
|
|
39
118
|
}, z.core.$strip>>;
|
|
40
119
|
}, z.core.$strip>;
|
|
41
120
|
export type AskToolInput = z.infer<typeof askSchema>;
|
|
@@ -54,6 +133,8 @@ export interface AskToolDetails {
|
|
|
54
133
|
multi?: boolean;
|
|
55
134
|
selectedOptions?: string[];
|
|
56
135
|
customInput?: string;
|
|
136
|
+
/** Set when the user answered with a clarification question instead of a choice. */
|
|
137
|
+
clarificationQuestion?: string;
|
|
57
138
|
/** Multi-part question mode */
|
|
58
139
|
results?: QuestionResult[];
|
|
59
140
|
}
|
|
@@ -80,13 +161,92 @@ export declare class AskTool implements AgentTool<typeof askSchema, AskToolDetai
|
|
|
80
161
|
}, z.core.$strip>>;
|
|
81
162
|
multi: z.ZodOptional<z.ZodBoolean>;
|
|
82
163
|
recommended: z.ZodOptional<z.ZodNumber>;
|
|
83
|
-
|
|
164
|
+
workflowGate: z.ZodOptional<z.ZodObject<{
|
|
165
|
+
stage: z.ZodEnum<{
|
|
166
|
+
"deep-interview": "deep-interview";
|
|
167
|
+
ralplan: "ralplan";
|
|
168
|
+
ultragoal: "ultragoal";
|
|
169
|
+
}>;
|
|
170
|
+
kind: z.ZodEnum<{
|
|
171
|
+
question: "question";
|
|
172
|
+
approval: "approval";
|
|
173
|
+
execution: "execution";
|
|
174
|
+
}>;
|
|
175
|
+
}, z.core.$strip>>;
|
|
176
|
+
deepInterview: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
177
|
+
round: z.ZodLiteral<0>;
|
|
178
|
+
component: z.ZodLiteral<"review-topology">;
|
|
179
|
+
intent_contract: z.ZodObject<{
|
|
180
|
+
items: z.ZodArray<z.ZodObject<{
|
|
181
|
+
id: z.ZodString;
|
|
182
|
+
category: z.ZodEnum<{
|
|
183
|
+
surface: "surface";
|
|
184
|
+
artifact: "artifact";
|
|
185
|
+
integration: "integration";
|
|
186
|
+
constraint: "constraint";
|
|
187
|
+
}>;
|
|
188
|
+
statement: z.ZodString;
|
|
189
|
+
}, z.core.$strict>>;
|
|
190
|
+
confirmation_options: z.ZodArray<z.ZodString>;
|
|
191
|
+
}, z.core.$strict>;
|
|
84
192
|
round_id: z.ZodOptional<z.ZodString>;
|
|
193
|
+
dimension: z.ZodString;
|
|
194
|
+
ambiguity: z.ZodNumber;
|
|
195
|
+
confused_terms: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
196
|
+
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
197
|
+
reference_id: z.ZodString;
|
|
198
|
+
label: z.ZodString;
|
|
199
|
+
origin: z.ZodString;
|
|
200
|
+
url: z.ZodOptional<z.ZodString>;
|
|
201
|
+
excerpt: z.ZodOptional<z.ZodString>;
|
|
202
|
+
}, z.core.$strict>>>;
|
|
203
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
85
204
|
round: z.ZodNumber;
|
|
205
|
+
intent_review: z.ZodObject<{
|
|
206
|
+
observed_items: z.ZodArray<z.ZodObject<{
|
|
207
|
+
id: z.ZodString;
|
|
208
|
+
category: z.ZodEnum<{
|
|
209
|
+
surface: "surface";
|
|
210
|
+
artifact: "artifact";
|
|
211
|
+
integration: "integration";
|
|
212
|
+
constraint: "constraint";
|
|
213
|
+
}>;
|
|
214
|
+
statement: z.ZodString;
|
|
215
|
+
}, z.core.$strict>>;
|
|
216
|
+
supporting_substitutions: z.ZodArray<z.ZodObject<{
|
|
217
|
+
removed_id: z.ZodString;
|
|
218
|
+
replacement_ids: z.ZodArray<z.ZodString>;
|
|
219
|
+
rationale: z.ZodString;
|
|
220
|
+
}, z.core.$strict>>;
|
|
221
|
+
approval_options: z.ZodArray<z.ZodString>;
|
|
222
|
+
}, z.core.$strict>;
|
|
223
|
+
round_id: z.ZodOptional<z.ZodString>;
|
|
86
224
|
component: z.ZodString;
|
|
87
225
|
dimension: z.ZodString;
|
|
88
226
|
ambiguity: z.ZodNumber;
|
|
89
|
-
|
|
227
|
+
confused_terms: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
228
|
+
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
229
|
+
reference_id: z.ZodString;
|
|
230
|
+
label: z.ZodString;
|
|
231
|
+
origin: z.ZodString;
|
|
232
|
+
url: z.ZodOptional<z.ZodString>;
|
|
233
|
+
excerpt: z.ZodOptional<z.ZodString>;
|
|
234
|
+
}, z.core.$strict>>>;
|
|
235
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
236
|
+
round_id: z.ZodOptional<z.ZodString>;
|
|
237
|
+
round: z.ZodNumber;
|
|
238
|
+
component: z.ZodString;
|
|
239
|
+
dimension: z.ZodString;
|
|
240
|
+
ambiguity: z.ZodNumber;
|
|
241
|
+
confused_terms: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
242
|
+
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
243
|
+
reference_id: z.ZodString;
|
|
244
|
+
label: z.ZodString;
|
|
245
|
+
origin: z.ZodString;
|
|
246
|
+
url: z.ZodOptional<z.ZodString>;
|
|
247
|
+
excerpt: z.ZodOptional<z.ZodString>;
|
|
248
|
+
}, z.core.$strict>>>;
|
|
249
|
+
}, z.core.$strict>]>>;
|
|
90
250
|
}, z.core.$strip>>;
|
|
91
251
|
}, z.core.$strip>;
|
|
92
252
|
readonly strict = true;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@sayknow-cli/coding-agent",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.2",
|
|
5
5
|
"description": "Sayknow-CLI CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://sayknow-cli.com",
|
|
7
7
|
"author": "jaybeyond",
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
53
53
|
"@babel/parser": "^7.29.3",
|
|
54
54
|
"@mozilla/readability": "^0.6.0",
|
|
55
|
-
"@sayknow-cli/stats": "0.5.
|
|
56
|
-
"@sayknow-cli/agent-core": "0.5.
|
|
57
|
-
"@sayknow-cli/ai": "0.5.
|
|
58
|
-
"@sayknow-cli/natives": "0.5.
|
|
59
|
-
"@sayknow-cli/tui": "0.5.
|
|
60
|
-
"@sayknow-cli/utils": "0.5.
|
|
55
|
+
"@sayknow-cli/stats": "0.5.2",
|
|
56
|
+
"@sayknow-cli/agent-core": "0.5.2",
|
|
57
|
+
"@sayknow-cli/ai": "0.5.2",
|
|
58
|
+
"@sayknow-cli/natives": "0.5.2",
|
|
59
|
+
"@sayknow-cli/tui": "0.5.2",
|
|
60
|
+
"@sayknow-cli/utils": "0.5.2",
|
|
61
61
|
"@puppeteer/browsers": "^2.13.0",
|
|
62
62
|
"@types/turndown": "5.0.6",
|
|
63
63
|
"@xterm/headless": "^6.0.0",
|
|
@@ -124,6 +124,9 @@
|
|
|
124
124
|
"./commands/skc-runtime-bridge": null,
|
|
125
125
|
"./capability/mcp": null,
|
|
126
126
|
"./config/mcp-schema": null,
|
|
127
|
+
"./config/model-auth": null,
|
|
128
|
+
"./config/model-bindings-applier": null,
|
|
129
|
+
"./config/model-discovery-manager": null,
|
|
127
130
|
"./discovery/mcp-json": null,
|
|
128
131
|
"./exa": null,
|
|
129
132
|
"./exa/*": null,
|
package/src/commands/session.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { Args, Command, Flags } from "@sayknow-cli/utils/cli";
|
|
2
|
+
import { readBootGeneration } from "../skc-runtime/boot-generation";
|
|
3
|
+
import {
|
|
4
|
+
decodeRestoreReference,
|
|
5
|
+
encodeRestoreReference,
|
|
6
|
+
evaluateRestoreCandidates,
|
|
7
|
+
listRestorePointers,
|
|
8
|
+
type RestoreCandidateVerdict,
|
|
9
|
+
} from "../skc-runtime/session-restore";
|
|
10
|
+
import { buildRestoreCandidateDeps, restoreSession } from "../skc-runtime/session-restore-runtime";
|
|
2
11
|
import {
|
|
3
12
|
attachSkcTmuxSession,
|
|
4
13
|
createSkcTmuxSession,
|
|
@@ -45,12 +54,13 @@ function sessionJson(session: SessionJsonDto): SessionJsonDto {
|
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
export default class Session extends Command {
|
|
48
|
-
static description =
|
|
57
|
+
static description =
|
|
58
|
+
"List, inspect, attach, and remove tagged SKC-managed tmux sessions. `restore` is a manual, opt-in recovery step that only ever restores sessions this machine can prove died in a REBOOT: nothing starts automatically at login, a session that is still live is never duplicated, and running tools or background work are not recovered — only the tmux session, its directory, and its conversation.";
|
|
49
59
|
static strict = false;
|
|
50
60
|
|
|
51
61
|
static args = {
|
|
52
62
|
action: Args.string({
|
|
53
|
-
description: "list (default), status, create, attach, or
|
|
63
|
+
description: "list (default), status, create, attach, remove, or restore",
|
|
54
64
|
required: false,
|
|
55
65
|
}),
|
|
56
66
|
session: Args.string({
|
|
@@ -67,6 +77,15 @@ export default class Session extends Command {
|
|
|
67
77
|
"state-file": Flags.string({
|
|
68
78
|
description: "Expected @skc-session-state-file tag for force-close (defense-in-depth match)",
|
|
69
79
|
}),
|
|
80
|
+
"dry-run": Flags.boolean({
|
|
81
|
+
description:
|
|
82
|
+
"restore: list candidates and their verdicts. Changes nothing at all (no tmux command, no lock, no database, no pointer write). This is the default when no --reference is given.",
|
|
83
|
+
default: false,
|
|
84
|
+
}),
|
|
85
|
+
reference: Flags.string({
|
|
86
|
+
description:
|
|
87
|
+
"restore: restore exactly one candidate by the opaque reference shown in --dry-run output. Refused unless that candidate is still reboot-eligible at this moment.",
|
|
88
|
+
}),
|
|
70
89
|
};
|
|
71
90
|
|
|
72
91
|
static examples = [
|
|
@@ -76,6 +95,8 @@ export default class Session extends Command {
|
|
|
76
95
|
"skc session attach <session>",
|
|
77
96
|
"skc session remove <session>",
|
|
78
97
|
"skc session force-close <session> --session-id <id>",
|
|
98
|
+
"skc session restore --dry-run # list what a reboot left behind",
|
|
99
|
+
"skc session restore --reference <ref> # restore exactly that one",
|
|
79
100
|
];
|
|
80
101
|
|
|
81
102
|
async run(): Promise<void> {
|
|
@@ -105,6 +126,71 @@ export default class Session extends Command {
|
|
|
105
126
|
return;
|
|
106
127
|
}
|
|
107
128
|
|
|
129
|
+
if (action === "restore") {
|
|
130
|
+
// Manual, reboot-proven, opt-in. Nothing here starts automatically at
|
|
131
|
+
// login, and a session is only ever restored when this host can prove
|
|
132
|
+
// the machine rebooted since that session was recorded.
|
|
133
|
+
const reference = flags.reference;
|
|
134
|
+
const decoded = reference ? decodeRestoreReference(reference) : null;
|
|
135
|
+
if (reference && !decoded) throw new Error("invalid_restore_reference");
|
|
136
|
+
const pointers = listRestorePointers().filter(
|
|
137
|
+
pointer =>
|
|
138
|
+
!decoded ||
|
|
139
|
+
(pointer.coordinator_session_id === decoded.coordinatorSessionId &&
|
|
140
|
+
pointer.state_file === decoded.stateFile),
|
|
141
|
+
);
|
|
142
|
+
if (reference && pointers.length === 0) throw new Error("restore_reference_not_found");
|
|
143
|
+
const verdicts = evaluateRestoreCandidates(pointers, buildRestoreCandidateDeps(readBootGeneration()));
|
|
144
|
+
const describe = (verdict: RestoreCandidateVerdict) => ({
|
|
145
|
+
reference: encodeRestoreReference(verdict.pointer.coordinator_session_id, verdict.pointer.state_file),
|
|
146
|
+
sessionId: verdict.pointer.skc_session_id,
|
|
147
|
+
cwd: verdict.pointer.cwd,
|
|
148
|
+
branch: verdict.pointer.branch,
|
|
149
|
+
eligible: verdict.eligible,
|
|
150
|
+
...(verdict.eligible ? {} : { reason: verdict.reason }),
|
|
151
|
+
});
|
|
152
|
+
// Dry run is the default reporting surface and performs zero mutation:
|
|
153
|
+
// no pointer write, no lock, no SQLite, no tmux command.
|
|
154
|
+
if (flags["dry-run"] || !reference) {
|
|
155
|
+
const rows = verdicts.map(describe);
|
|
156
|
+
if (json) {
|
|
157
|
+
writeJson({ ok: true, dryRun: true, candidates: rows });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
writeText(
|
|
161
|
+
rows.length === 0
|
|
162
|
+
? ["no restore candidates"]
|
|
163
|
+
: rows.map(row =>
|
|
164
|
+
[
|
|
165
|
+
row.eligible ? "restorable" : `skipped(${row.reason})`,
|
|
166
|
+
row.sessionId,
|
|
167
|
+
row.cwd,
|
|
168
|
+
row.reference,
|
|
169
|
+
].join("\t"),
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const verdict = verdicts[0];
|
|
175
|
+
if (!verdict) throw new Error("restore_reference_not_found");
|
|
176
|
+
if (!verdict.eligible) throw new Error(`restore_ineligible_${verdict.reason}`);
|
|
177
|
+
const outcome = restoreSession(verdict.pointer);
|
|
178
|
+
if (!outcome.ok) throw new Error(outcome.detail);
|
|
179
|
+
if (json) {
|
|
180
|
+
writeJson({
|
|
181
|
+
ok: true,
|
|
182
|
+
restored: {
|
|
183
|
+
sessionId: outcome.pointer.skc_session_id,
|
|
184
|
+
cwd: outcome.pointer.cwd,
|
|
185
|
+
tmuxSession: outcome.tmuxSession,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
writeText([`restored: ${outcome.tmuxSession} (${outcome.pointer.skc_session_id})`]);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
108
194
|
if (action === "create") {
|
|
109
195
|
const session = createSkcTmuxSession();
|
|
110
196
|
if (json) {
|
|
@@ -1032,6 +1032,13 @@ export class ModelRegistry {
|
|
|
1032
1032
|
#lastStaticLoadMtime: number | null = null;
|
|
1033
1033
|
#registeredProviderSources: Set<string> = new Set();
|
|
1034
1034
|
#providerDiscoveryStates: Map<string, ProviderDiscoveryState> = new Map();
|
|
1035
|
+
/**
|
|
1036
|
+
* Monotonic per-provider discovery sequence. Overlapping refreshes for the
|
|
1037
|
+
* same provider can complete out of order; only the newest-started refresh
|
|
1038
|
+
* may publish discovery state or contribute models, so a slow stale fetch
|
|
1039
|
+
* never overwrites a fresher result.
|
|
1040
|
+
*/
|
|
1041
|
+
#providerDiscoverySequences: Map<string, number> = new Map();
|
|
1035
1042
|
#cacheDbPath?: string;
|
|
1036
1043
|
#suppressedSelectors: Map<string, number> = new Map();
|
|
1037
1044
|
#backgroundRefresh?: Promise<void>;
|
|
@@ -1815,11 +1822,15 @@ export class ModelRegistry {
|
|
|
1815
1822
|
providerConfig: DiscoveryProviderConfig,
|
|
1816
1823
|
strategy: ModelRefreshStrategy,
|
|
1817
1824
|
): Promise<Model<Api>[]> {
|
|
1825
|
+
const sequence = (this.#providerDiscoverySequences.get(providerConfig.provider) ?? 0) + 1;
|
|
1826
|
+
this.#providerDiscoverySequences.set(providerConfig.provider, sequence);
|
|
1827
|
+
const isCurrentDiscovery = () => this.#providerDiscoverySequences.get(providerConfig.provider) === sequence;
|
|
1818
1828
|
const cached = readModelCache<Api>(providerConfig.provider, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
|
|
1819
1829
|
const requiresAuth = !this.#keylessProviders.has(providerConfig.provider);
|
|
1820
1830
|
if (requiresAuth) {
|
|
1821
1831
|
const apiKey = await this.#peekApiKeyForProvider(providerConfig.provider);
|
|
1822
1832
|
if (!isAuthenticated(apiKey)) {
|
|
1833
|
+
if (!isCurrentDiscovery()) return [];
|
|
1823
1834
|
this.#providerDiscoveryStates.set(providerConfig.provider, {
|
|
1824
1835
|
provider: providerConfig.provider,
|
|
1825
1836
|
status: "unauthenticated",
|
|
@@ -1865,6 +1876,7 @@ export class ModelRegistry {
|
|
|
1865
1876
|
: result.models.length > 0
|
|
1866
1877
|
? "ok"
|
|
1867
1878
|
: "empty";
|
|
1879
|
+
if (!isCurrentDiscovery()) return [];
|
|
1868
1880
|
this.#providerDiscoveryStates.set(providerId, {
|
|
1869
1881
|
provider: providerId,
|
|
1870
1882
|
status,
|
|
@@ -10,11 +10,18 @@
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import { getConfigRootDir, isCompiledBinary, logger } from "@sayknow-cli/utils";
|
|
13
|
-
import { settings } from "./settings";
|
|
13
|
+
import { settings as globalSettings, type Settings } from "./settings";
|
|
14
14
|
import type { TelegramSettings } from "./settings-schema";
|
|
15
15
|
import { telegramSettingsToEnv, validateTelegramSettings } from "./telegram-env-bridge";
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The startup command runs against an explicit Settings instance (tests and
|
|
19
|
+
* embedders inject one), so read through it instead of the global proxy —
|
|
20
|
+
* which may not be initialized at all on those paths.
|
|
21
|
+
*/
|
|
22
|
+
type TelegramSettingsSource = Pick<Settings, "get">;
|
|
23
|
+
|
|
24
|
+
function readTelegramSettings(settings: TelegramSettingsSource): TelegramSettings {
|
|
18
25
|
return {
|
|
19
26
|
enabled: settings.get("telegram.enabled"),
|
|
20
27
|
botToken: settings.get("telegram.botToken"),
|
|
@@ -69,10 +76,10 @@ function isAlreadyRunning(pidFile: string): boolean {
|
|
|
69
76
|
* it in the background. Never throws — failures are logged as warnings so the
|
|
70
77
|
* main skc session is unaffected.
|
|
71
78
|
*/
|
|
72
|
-
export async function maybeAutostartTelegramRemote(): Promise<void> {
|
|
79
|
+
export async function maybeAutostartTelegramRemote(settings: TelegramSettingsSource = globalSettings): Promise<void> {
|
|
73
80
|
if (!settings.get("telegram.enabled")) return;
|
|
74
81
|
|
|
75
|
-
const config = readTelegramSettings();
|
|
82
|
+
const config = readTelegramSettings(settings);
|
|
76
83
|
const errors = validateTelegramSettings(config);
|
|
77
84
|
if (errors.length > 0) {
|
|
78
85
|
logger.warn("Telegram Remote enabled but misconfigured, skipping autostart", { errors });
|