@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
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,163 @@ This file tracks the **fork's own releases**; upstream's full feature history li
|
|
|
5
5
|
in that project. Each release notes the upstream version it is built on.
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.5.2] — 2026-08-14
|
|
11
|
+
|
|
12
|
+
Built on upstream **gajae-code v0.12.0**.
|
|
13
|
+
|
|
14
|
+
### Fixed (unattended workflow gates were unanswerable through the control plane)
|
|
15
|
+
|
|
16
|
+
`UnattendedSessionControlPlane.emitGate` opened gates without a broker
|
|
17
|
+
continuation, so every gate it emitted was quarantined as
|
|
18
|
+
`opened_without_continuation` and could never be answered; its `advance` hook
|
|
19
|
+
also settled the waiter early, violating the broker's post-advance liveness
|
|
20
|
+
check, and accepted gates then failed terminalization for lack of a proof. The
|
|
21
|
+
control plane now registers a live continuation, settles waiters in
|
|
22
|
+
`completeAccepted` (mirroring the session-side emitter), and declares the
|
|
23
|
+
designed `not_published` terminal proof. The ask tool regained the fork's gate
|
|
24
|
+
semantics on top of that: a negotiated unattended emitter wins over an attended
|
|
25
|
+
UI, clarification answers surface the user's question instead of aborting as a
|
|
26
|
+
cancel, multi-select keeps its empty-"Next" semantics through the gate schema,
|
|
27
|
+
and gate metadata carries the Next/Done navigation label again.
|
|
28
|
+
|
|
29
|
+
### Fixed (ask wire schema: deep-interview intent branches and gate addressing)
|
|
30
|
+
|
|
31
|
+
The deep-interview intent contract is enforced **on the wire** again: ask's
|
|
32
|
+
`deepInterview` metadata is a union of three mutually exclusive strict branches
|
|
33
|
+
(ordinary round / Round-0 `intent_contract` locked to `component:
|
|
34
|
+
"review-topology"` / post-Round-0 `intent_review` with `round >= 1`), so a
|
|
35
|
+
provider that only sees the JSON schema cannot combine a manifest lock with a
|
|
36
|
+
reduction review or attach either to the wrong round. Questions also regained
|
|
37
|
+
the `workflowGate` stage/kind override for addressing non-deep-interview gates,
|
|
38
|
+
and such overridden questions are excluded from the interview recorder.
|
|
39
|
+
|
|
40
|
+
### Fixed (`SKC_PY` / `PI_PY` / `PI_JS` eval backend selection was ignored)
|
|
41
|
+
|
|
42
|
+
`resolveEvalBackends` existed but the eval tool never called it: backend
|
|
43
|
+
allowance was read from settings only, so `SKC_PY=js` still spawned the Python
|
|
44
|
+
kernel. The tool now resolves allowance through the documented precedence
|
|
45
|
+
(`SKC_PY`, then legacy `PI_PY`/`PI_JS`, then `eval.py`/`eval.js` settings).
|
|
46
|
+
|
|
47
|
+
### Fixed (stale model discovery result could overwrite a fresher one)
|
|
48
|
+
|
|
49
|
+
Overlapping `refreshProvider` calls for the same provider raced without any
|
|
50
|
+
ordering guard: whichever fetch *completed* last published its discovery state
|
|
51
|
+
and model list, so a slow stale response could erase the models a fresher
|
|
52
|
+
refresh had just delivered. Discovery now carries a monotonic per-provider
|
|
53
|
+
sequence and only the newest-started refresh may publish state or contribute
|
|
54
|
+
models.
|
|
55
|
+
|
|
56
|
+
### Fixed (post-merge repair: type-check and test contracts realigned)
|
|
57
|
+
|
|
58
|
+
The upstream merges left the workspace `check` red for several releases: test
|
|
59
|
+
files frozen at fork v0.4.7 kept exercising APIs their source had since dropped
|
|
60
|
+
(session-sticky canonical model resolution, `getSelectorSuppressionStatus`,
|
|
61
|
+
`refreshPresetProfiles`, the pre-broker in-process `AcpAgent`, the fork-era
|
|
62
|
+
bridge-client `WorkflowGate` surface). Stale suites superseded by current
|
|
63
|
+
coverage were removed, the survivors were realigned to the shipped APIs, the
|
|
64
|
+
form-elicitation bridge kept its only coverage via a ported focused suite, the
|
|
65
|
+
`node-pty` dev dependency the merge dropped from `@sayknow-cli/tui` is restored,
|
|
66
|
+
and the extracted model helper modules (`config/model-auth`,
|
|
67
|
+
`config/model-bindings-applier`, `config/model-discovery-manager`) are now
|
|
68
|
+
explicitly unexported, matching the documented package surface.
|
|
69
|
+
|
|
70
|
+
### Fixed (todo_write truncation and retry loops)
|
|
71
|
+
|
|
72
|
+
Structurally complete JSON tool calls now execute even when a Responses provider
|
|
73
|
+
hits its output-token limit before emitting the final item event. Invalid todo
|
|
74
|
+
payloads retry at most once, while transport failures and repeated errors fail
|
|
75
|
+
open so the requested work continues without visible todo tracking.
|
|
76
|
+
|
|
77
|
+
### Fixed (v0.5.0 upstream merge reverted the fork's tmux graphics support)
|
|
78
|
+
|
|
79
|
+
The v0.12.0 upstream merge overwrote `packages/tui/src/tui.ts` and silently
|
|
80
|
+
dropped three fork-only behaviors, which is why the pet was unavailable in every
|
|
81
|
+
tmux session and `packages/tui/test/sixel-probe.test.ts` shipped with three
|
|
82
|
+
failing tests:
|
|
83
|
+
|
|
84
|
+
- the capability probe refused to run under any multiplexer, so
|
|
85
|
+
`isSixelMultiplexerEnabled()` became dead code
|
|
86
|
+
- the sixel probe was no longer wrapped in tmux's DCS passthrough envelope, so
|
|
87
|
+
tmux answered for a client it knows nothing about
|
|
88
|
+
- the `CSI 16 t` cell-size query lost its passthrough wrapper (the v0.4.6 fix),
|
|
89
|
+
so tmux reported a cell size the outer terminal never uses
|
|
90
|
+
|
|
91
|
+
All three are restored, with the probe deadline back at 600 ms under tmux to
|
|
92
|
+
cover the passthrough round trip.
|
|
93
|
+
|
|
94
|
+
### Fixed (transcript images stacked over the text under tmux)
|
|
95
|
+
|
|
96
|
+
Enabling the sixel probe under tmux also switched INLINE graphics on, and the
|
|
97
|
+
inline path wrapped every raster in the DCS passthrough envelope. Passthrough
|
|
98
|
+
writes pixels straight into the OUTER terminal's image plane at its *physical*
|
|
99
|
+
cursor: tmux neither positions that cursor for the pane nor records the pixels,
|
|
100
|
+
so tool-result screenshots landed in the wrong row and survived every repaint —
|
|
101
|
+
each render stacked another copy over the transcript.
|
|
102
|
+
|
|
103
|
+
Inline placements now follow the same ownership rule the pet already used:
|
|
104
|
+
|
|
105
|
+
- tmux advertising the `sixel` terminal-feature (which the SKC tmux profile sets
|
|
106
|
+
automatically) parses the raster into its own screen model, so the raster is
|
|
107
|
+
written raw and scroll / erase / resize move the image with its text.
|
|
108
|
+
- Without that feature the inline render returns the `[image/png …]` placeholder
|
|
109
|
+
instead. An image welded to the outer terminal's physical cursor for the rest
|
|
110
|
+
of the session is worse than no image.
|
|
111
|
+
- Absolutely positioned overlays (the pet) are unaffected: they carry their own
|
|
112
|
+
coordinates through the envelope and remain the only passthrough user.
|
|
113
|
+
|
|
114
|
+
### Added (Sayknow Pet under tmux on kitty-protocol terminals)
|
|
115
|
+
|
|
116
|
+
Ghostty, Kitty and WezTerm implement kitty graphics and no sixel at all, so the
|
|
117
|
+
sixel probe alone left them with no pet inside tmux. SKC now also forwards a
|
|
118
|
+
kitty capability query (`a=q`) through the passthrough envelope. tmux cannot
|
|
119
|
+
answer it on the terminal's behalf — it does not implement the protocol — so an
|
|
120
|
+
`OK` coming back is genuine end-to-end evidence, unlike tmux's compile-time DA1
|
|
121
|
+
sixel claim.
|
|
122
|
+
|
|
123
|
+
- A successful query enables a dedicated **overlay** channel
|
|
124
|
+
(`setTmuxOverlayImageProtocol`) rather than inline image rendering: absolutely
|
|
125
|
+
positioned art can carry its own cursor addressing through the envelope, while
|
|
126
|
+
inline placements would land at tmux's stale physical cursor.
|
|
127
|
+
- Overlay payloads now carry the pane origin (`#{pane_top}`, `#{pane_left}`, and
|
|
128
|
+
top status lines), so a split window or a top status bar no longer draws the
|
|
129
|
+
pet in the wrong place. This also fixes the pre-existing sixel-fallback path.
|
|
130
|
+
- `allow-passthrough` is requested pane-locally at startup, so a tmux pane the
|
|
131
|
+
user created by hand works without manual configuration.
|
|
132
|
+
- Kill switch: `SKC_KITTY_MULTIPLEXER=0`.
|
|
133
|
+
- The tmux-specific unavailable warning no longer tells users to leave the
|
|
134
|
+
multiplexer or to force sixel; it names the actual requirement.
|
|
135
|
+
|
|
136
|
+
Verified end to end on Ghostty 1.3.1 + tmux 3.6b: pane passthrough is enabled
|
|
137
|
+
automatically, the probe reports `ImageProtocol.Kitty`, and the outer terminal's
|
|
138
|
+
real cell metrics (16x34) replace the 9x18 default.
|
|
139
|
+
|
|
140
|
+
## [0.5.1] — 2026-07-29
|
|
141
|
+
|
|
142
|
+
Built on upstream **gajae-code v0.12.0**.
|
|
143
|
+
|
|
144
|
+
### Fixed (Sayknow Pet still left sixel residue under tmux)
|
|
145
|
+
|
|
146
|
+
0.5.0 gave tmux ownership of the pet's sixel, which is the real fix — but it only
|
|
147
|
+
takes effect for a client that attaches *after* the feature is set, because tmux
|
|
148
|
+
computes client features at attach time. Every already-attached client kept
|
|
149
|
+
falling back to DCS passthrough, and that fallback still erased at tmux level
|
|
150
|
+
while its pixels lived in the outer terminal. So the residue persisted for
|
|
151
|
+
exactly the people who already had a session open.
|
|
152
|
+
|
|
153
|
+
- **The erase now rides the same envelope as the draw.** When the frame goes out
|
|
154
|
+
through passthrough, the erase does too, reaching the terminal that actually
|
|
155
|
+
holds the image; the absolute coordinates match because the draw used the same
|
|
156
|
+
ones. When tmux owns the sixel, the erase stays at tmux level as before.
|
|
157
|
+
- **`terminal-features` is set with the correct scope.** It is a *server* option,
|
|
158
|
+
so the previous `set-option -aq -t <target>` silently ignored the target. Now
|
|
159
|
+
`set-option -saq`.
|
|
160
|
+
- Added a regression test that pins the multiplexer environment and asserts draw
|
|
161
|
+
and erase share one envelope. The pet suite previously inherited whatever
|
|
162
|
+
terminal the developer ran it in, so it passed on CI and failed inside tmux;
|
|
163
|
+
the environment is now pinned per test.
|
|
164
|
+
|
|
8
165
|
## [0.5.0] — 2026-07-28
|
|
9
166
|
|
|
10
167
|
Built on upstream **gajae-code v0.12.0** (previous fork release tracked v0.11.6).
|
|
@@ -24,6 +24,13 @@ export default class Session extends Command {
|
|
|
24
24
|
"state-file": import("@sayknow-cli/utils/cli").FlagDescriptor<"string"> & {
|
|
25
25
|
description: string;
|
|
26
26
|
};
|
|
27
|
+
"dry-run": import("@sayknow-cli/utils/cli").FlagDescriptor<"boolean"> & {
|
|
28
|
+
description: string;
|
|
29
|
+
default: boolean;
|
|
30
|
+
};
|
|
31
|
+
reference: import("@sayknow-cli/utils/cli").FlagDescriptor<"string"> & {
|
|
32
|
+
description: string;
|
|
33
|
+
};
|
|
27
34
|
};
|
|
28
35
|
static examples: string[];
|
|
29
36
|
run(): Promise<void>;
|
|
@@ -6,9 +6,17 @@
|
|
|
6
6
|
* process, tracks its PID so repeated skc invocations don't double-spawn,
|
|
7
7
|
* and redirects output to a log file under the config root.
|
|
8
8
|
*/
|
|
9
|
+
import { type Settings } from "./settings";
|
|
10
|
+
/**
|
|
11
|
+
* The startup command runs against an explicit Settings instance (tests and
|
|
12
|
+
* embedders inject one), so read through it instead of the global proxy —
|
|
13
|
+
* which may not be initialized at all on those paths.
|
|
14
|
+
*/
|
|
15
|
+
type TelegramSettingsSource = Pick<Settings, "get">;
|
|
9
16
|
/**
|
|
10
17
|
* If `telegram.enabled` is true and the gateway is not already running, spawn
|
|
11
18
|
* it in the background. Never throws — failures are logged as warnings so the
|
|
12
19
|
* main skc session is unaffected.
|
|
13
20
|
*/
|
|
14
|
-
export declare function maybeAutostartTelegramRemote(): Promise<void>;
|
|
21
|
+
export declare function maybeAutostartTelegramRemote(settings?: TelegramSettingsSource): Promise<void>;
|
|
22
|
+
export {};
|
|
@@ -9,15 +9,16 @@ export declare function isPetAvailable(): boolean;
|
|
|
9
9
|
export declare function createPetSelectItems(options: ReadonlyArray<SelectItem>, currentValue: string, available: boolean): SelectItem[];
|
|
10
10
|
/**
|
|
11
11
|
* Grace period before declaring the terminal pet-incapable at startup. The
|
|
12
|
-
* asynchronous
|
|
13
|
-
*
|
|
14
|
-
* supported terminal is never told it is
|
|
15
|
-
* still in flight.
|
|
12
|
+
* asynchronous capability probes start inside `TUI.start()` and answer within
|
|
13
|
+
* their own deadline (250 ms directly, 600 ms through tmux passthrough); this
|
|
14
|
+
* margin covers probe scheduling so a supported terminal is never told it is
|
|
15
|
+
* incompatible while a probe is still in flight.
|
|
16
16
|
*/
|
|
17
|
-
export declare const PET_CAPABILITY_SETTLE_MS =
|
|
17
|
+
export declare const PET_CAPABILITY_SETTLE_MS = 1500;
|
|
18
18
|
/**
|
|
19
|
-
* Whether
|
|
20
|
-
* graphics for this session, meaning current
|
|
19
|
+
* Whether an asynchronous startup capability probe (Sixel, or Kitty through
|
|
20
|
+
* tmux passthrough) may still enable graphics for this session, meaning current
|
|
21
|
+
* unavailability is not final.
|
|
21
22
|
*/
|
|
22
23
|
export declare function isPetCapabilityProbePending(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
|
23
24
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Container, type SelectList } from "@sayknow-cli/tui";
|
|
2
2
|
import type { PetMode } from "./sayknow-pet-widget";
|
|
3
3
|
/**
|
|
4
|
-
* Theme-style picker for the sayknow pet skin (Off /
|
|
4
|
+
* Theme-style picker for the sayknow pet skin (Off / RedOctopus / BlueOctopus). Preview
|
|
5
5
|
* fires as the selection moves; select commits, cancel restores.
|
|
6
6
|
*/
|
|
7
7
|
export declare class PetSelectorComponent extends Container {
|
|
@@ -50,7 +50,7 @@ export declare class SayknowPetWidget {
|
|
|
50
50
|
setMode(mode: PetMode): void;
|
|
51
51
|
/** Live preview during a selector: change the sprite without re-mounting the
|
|
52
52
|
* composer editor (that would tear down the open overlay). After a short idle
|
|
53
|
-
* eye-roll it fires the signature burst once (
|
|
53
|
+
* eye-roll it fires the signature burst once (RedOctopus flex, BlueOctopus para-para
|
|
54
54
|
* then sob) so the selector demos the animation instead of waiting the random gap. */
|
|
55
55
|
previewMode(mode: PetMode): void;
|
|
56
56
|
commitPreviewMode(mode: PetMode): void;
|
|
@@ -67,6 +67,12 @@ export declare function modelSupportsTokenCostMetrics(model: Model | undefined):
|
|
|
67
67
|
export interface WorkflowGateEmitter {
|
|
68
68
|
/** True only when unattended mode has been negotiated. */
|
|
69
69
|
isUnattended(): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* True when gates emitted through this emitter can be answered remotely
|
|
72
|
+
* (workflow_gate_response over RPC/bridge). The ask tool refuses headless
|
|
73
|
+
* execution unless this reports true.
|
|
74
|
+
*/
|
|
75
|
+
supportsRemoteGateAnswers(): boolean;
|
|
70
76
|
/** Open + emit a gate; resolves with the agent's answer (from workflow_gate_response). */
|
|
71
77
|
emitGate(input: OpenGateInput): Promise<unknown>;
|
|
72
78
|
/**
|
|
@@ -107,6 +113,7 @@ export declare class UnattendedSessionControlPlane implements RpcUnattendedContr
|
|
|
107
113
|
private readonly opts;
|
|
108
114
|
constructor(opts: UnattendedSessionOptions);
|
|
109
115
|
isUnattended(): boolean;
|
|
116
|
+
supportsRemoteGateAnswers(): boolean;
|
|
110
117
|
/** Observe every emitted gate (e.g. so an extension can map an ask to its gate_id). */
|
|
111
118
|
onGateEmitted(listener: (gate: RpcWorkflowGate) => void): () => void;
|
|
112
119
|
get controller(): UnattendedRunController | undefined;
|
|
@@ -60,6 +60,8 @@ export interface AskSelectedAckRecoveryParticipant {
|
|
|
60
60
|
/** SDK-native surface for emitting a workflow gate and awaiting its answer. */
|
|
61
61
|
export interface WorkflowGateEmitter {
|
|
62
62
|
supportsRemoteGateAnswers(): boolean;
|
|
63
|
+
/** True when an unattended run has been negotiated for this emitter (control-plane emitters). */
|
|
64
|
+
isUnattended?(): boolean;
|
|
63
65
|
emitGate(input: OpenGateInput): Promise<unknown>;
|
|
64
66
|
onGateEmitted?(listener: (gate: WorkflowGate) => void): () => void;
|
|
65
67
|
resolveGate?(response: WorkflowGateResponse): Promise<WorkflowGateResolution>;
|
|
@@ -528,6 +528,7 @@ export declare class StreamingEditFileCache {
|
|
|
528
528
|
has(path: string): boolean;
|
|
529
529
|
get totalBytes(): number;
|
|
530
530
|
}
|
|
531
|
+
export declare function buildTodoWriteFailureReminder(errorText: string | undefined, failureCount: number): string;
|
|
531
532
|
export declare class AgentSession {
|
|
532
533
|
#private;
|
|
533
534
|
readonly agent: Agent;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot-generation evidence for reboot-only session restore.
|
|
3
|
+
*
|
|
4
|
+
* Restore exists to survive a reboot, and only a reboot. The eligibility rule is
|
|
5
|
+
* therefore narrow on purpose: a session may be restored only when the recorded
|
|
6
|
+
* and current boot values come from the SAME source and differ. Anything else —
|
|
7
|
+
* equal values, a missing or malformed record, an unreadable probe, or two
|
|
8
|
+
* different sources — is inconclusive and must never spawn.
|
|
9
|
+
*
|
|
10
|
+
* The same-source requirement is not pedantry. Linux can report `boot-id` at
|
|
11
|
+
* record time and fall back to `proc-btime` later; those two values are always
|
|
12
|
+
* unequal even on one boot, so comparing across sources would read every launch
|
|
13
|
+
* as a reboot and duplicate live sessions.
|
|
14
|
+
*/
|
|
15
|
+
/** Where a boot value came from. Values are only ever compared within one source. */
|
|
16
|
+
export type BootGenerationSource = "darwin-kern-boottime" | "linux-boot-id" | "linux-proc-btime" | "unavailable";
|
|
17
|
+
export interface BootGeneration {
|
|
18
|
+
source: BootGenerationSource;
|
|
19
|
+
/** Opaque; only equality within the same source is meaningful. */
|
|
20
|
+
value: string | null;
|
|
21
|
+
}
|
|
22
|
+
export type BootComparison = "changed" | "same_boot" | "boot_unknown";
|
|
23
|
+
export interface RecordedBootGeneration {
|
|
24
|
+
schema_version: number;
|
|
25
|
+
source: string;
|
|
26
|
+
value: string;
|
|
27
|
+
}
|
|
28
|
+
export interface BootGenerationProbeDeps {
|
|
29
|
+
platform?: NodeJS.Platform;
|
|
30
|
+
readFile?: (file: string) => string;
|
|
31
|
+
runCommand?: (command: string, args: string[]) => {
|
|
32
|
+
exitCode: number | null;
|
|
33
|
+
stdout: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** `{ sec = 1785305532, usec = 377197 } Wed Jul 29 ...` -> `1785305532.377197`. */
|
|
37
|
+
export declare function parseDarwinBootTime(raw: string): string | null;
|
|
38
|
+
/** A boot id is a UUID that changes on every boot; anything else is not usable. */
|
|
39
|
+
export declare function parseLinuxBootId(raw: string): string | null;
|
|
40
|
+
/** `/proc/stat` carries `btime <seconds>` once, as the kernel boot wall-clock. */
|
|
41
|
+
export declare function parseLinuxProcBtime(raw: string): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Reads the strongest boot evidence this platform offers.
|
|
44
|
+
*
|
|
45
|
+
* Windows returns `unavailable`: restore is unsupported there (no immutable
|
|
46
|
+
* native tmux session identity), so there is nothing to gate.
|
|
47
|
+
*/
|
|
48
|
+
export declare function readBootGeneration(deps?: BootGenerationProbeDeps): BootGeneration;
|
|
49
|
+
export declare function isBootValueWellFormed(source: string, value: string): boolean;
|
|
50
|
+
export declare function isRecordedBootGeneration(value: unknown): value is RecordedBootGeneration;
|
|
51
|
+
export declare function recordBootGeneration(current: BootGeneration): RecordedBootGeneration | null;
|
|
52
|
+
/**
|
|
53
|
+
* Decides whether the machine rebooted since the session was recorded.
|
|
54
|
+
*
|
|
55
|
+
* `changed` is the ONLY executable answer. It requires a well-formed record, a
|
|
56
|
+
* readable current probe, an identical source, and different values. Source
|
|
57
|
+
* mismatch is deliberately `boot_unknown` rather than `changed`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function compareBootGeneration(recorded: unknown, current: BootGeneration): BootComparison;
|
|
@@ -30,14 +30,16 @@ export interface TmuxSpawnResult {
|
|
|
30
30
|
exitCode: number | null;
|
|
31
31
|
signalCode?: string | null;
|
|
32
32
|
stderr?: string;
|
|
33
|
+
/** Populated only when the caller asked for `stdout: "pipe"`. */
|
|
34
|
+
stdout?: string;
|
|
33
35
|
}
|
|
34
36
|
export type TmuxSpawnSync = (command: string, args: string[], options: TmuxSpawnOptions) => TmuxSpawnResult;
|
|
35
37
|
export interface TmuxSpawnOptions {
|
|
36
38
|
cwd: string;
|
|
37
39
|
env: NodeJS.ProcessEnv;
|
|
38
40
|
stdin: "inherit";
|
|
39
|
-
stdout: "inherit";
|
|
40
|
-
stderr: "inherit";
|
|
41
|
+
stdout: "inherit" | "pipe";
|
|
42
|
+
stderr: "inherit" | "pipe";
|
|
41
43
|
}
|
|
42
44
|
export interface TmuxLaunchPlan {
|
|
43
45
|
tmuxCommand: string;
|
|
@@ -50,6 +52,12 @@ export interface TmuxLaunchPlan {
|
|
|
50
52
|
project?: string | null;
|
|
51
53
|
sessionId?: string | null;
|
|
52
54
|
sessionStateFile?: string | null;
|
|
55
|
+
/**
|
|
56
|
+
* Capability of the RESOLVED provider, not a platform guess. psmux has no
|
|
57
|
+
* immutable native session identity, so it stays outside the native-proof
|
|
58
|
+
* create fence and keeps its existing spawn/profile/attach behavior.
|
|
59
|
+
*/
|
|
60
|
+
isPsmux: boolean;
|
|
53
61
|
}
|
|
54
62
|
export interface SkcTmuxProfileResult {
|
|
55
63
|
skipped: boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { BootGeneration } from "./boot-generation";
|
|
2
|
+
import { type RestoreCandidateDeps, type RestorePointer, type RestoreSidecarFacts } from "./session-restore";
|
|
3
|
+
/**
|
|
4
|
+
* Strict re-read of the sidecar a pointer names.
|
|
5
|
+
*
|
|
6
|
+
* Returns null for missing, unreadable, or malformed content: restore must never
|
|
7
|
+
* infer a session's identity from the pointer alone.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readSidecarFacts(pointer: RestorePointer): RestoreSidecarFacts | null;
|
|
10
|
+
/**
|
|
11
|
+
* True when a tmux session already carries this exact coordinator identity.
|
|
12
|
+
*
|
|
13
|
+
* An unreadable tmux is reported as a collision on purpose: not being able to
|
|
14
|
+
* see the server is not evidence that the identity is free.
|
|
15
|
+
*/
|
|
16
|
+
export declare function hasLiveIdentity(pointer: RestorePointer, env?: NodeJS.ProcessEnv): boolean;
|
|
17
|
+
/** psmux exposes no immutable native session identity, so restore cannot prove ownership there. */
|
|
18
|
+
export declare function ownerProofAvailable(env?: NodeJS.ProcessEnv): boolean;
|
|
19
|
+
export declare function buildRestoreCandidateDeps(currentBoot: BootGeneration, env?: NodeJS.ProcessEnv): RestoreCandidateDeps;
|
|
20
|
+
export type RestoreOutcome = {
|
|
21
|
+
ok: true;
|
|
22
|
+
pointer: RestorePointer;
|
|
23
|
+
tmuxSession: string;
|
|
24
|
+
} | {
|
|
25
|
+
ok: false;
|
|
26
|
+
pointer: RestorePointer;
|
|
27
|
+
code: string;
|
|
28
|
+
detail: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Restores exactly one eligible candidate.
|
|
32
|
+
*
|
|
33
|
+
* Deliberately goes through the ordinary fenced creator rather than spawning
|
|
34
|
+
* tmux directly: restore must inherit the same identity fence, owner-isolation
|
|
35
|
+
* proof, and exact cleanup as every other producer. The only differences are the
|
|
36
|
+
* working directory and the `--resume` argv handed to the child.
|
|
37
|
+
*
|
|
38
|
+
* Never cleans up another owner's session. A fence refusal is reported and the
|
|
39
|
+
* candidate is skipped.
|
|
40
|
+
*/
|
|
41
|
+
export declare function restoreSession(pointer: RestorePointer, env?: NodeJS.ProcessEnv): RestoreOutcome;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { type BootGeneration, type RecordedBootGeneration } from "./boot-generation";
|
|
2
|
+
export declare const RESTORE_POINTER_SCHEMA_VERSION = 1;
|
|
3
|
+
export interface RestorePointer {
|
|
4
|
+
schema_version: number;
|
|
5
|
+
/** Coordinator identity: what the create fence and the sidecar are keyed by. */
|
|
6
|
+
coordinator_session_id: string;
|
|
7
|
+
state_file: string;
|
|
8
|
+
/** SKC session id whose transcript `skc --resume` would reopen. */
|
|
9
|
+
skc_session_id: string;
|
|
10
|
+
session_file: string;
|
|
11
|
+
cwd: string;
|
|
12
|
+
branch: string | null;
|
|
13
|
+
boot: RecordedBootGeneration;
|
|
14
|
+
updated_at: string;
|
|
15
|
+
}
|
|
16
|
+
/** Pointers live under SKC's own root so they are discoverable without scanning projects. */
|
|
17
|
+
export declare function restorePointerDirectory(): string;
|
|
18
|
+
export declare function restorePointerFile(coordinatorSessionId: string, stateFile: string): string;
|
|
19
|
+
export declare function isRestorePointer(value: unknown): value is RestorePointer;
|
|
20
|
+
/**
|
|
21
|
+
* The session id `skc --resume` resolves is the transcript header id, which is
|
|
22
|
+
* NOT the coordinator id: a normal `skc --tmux` child inherits only the
|
|
23
|
+
* coordinator identity and then mints its own session id. Reading it from the
|
|
24
|
+
* transcript is the only way a pointer can name the conversation that will
|
|
25
|
+
* actually be reopened.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readTranscriptSessionId(sessionFile: string): string | null;
|
|
28
|
+
export interface PublishRestorePointerInput {
|
|
29
|
+
coordinatorSessionId: string;
|
|
30
|
+
stateFile: string;
|
|
31
|
+
skcSessionId: string;
|
|
32
|
+
sessionFile: string;
|
|
33
|
+
cwd: string;
|
|
34
|
+
branch?: string | null;
|
|
35
|
+
bootGeneration?: BootGeneration;
|
|
36
|
+
now?: () => Date;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Publishes (or refreshes) the pointer for a live session.
|
|
40
|
+
*
|
|
41
|
+
* Returns false without writing when the platform cannot produce boot evidence:
|
|
42
|
+
* a pointer whose boot value is unusable could never be judged `changed`, so
|
|
43
|
+
* writing one would only add noise. Never throws — losing a pointer must not
|
|
44
|
+
* break the session that was trying to publish it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function publishRestorePointer(input: PublishRestorePointerInput): boolean;
|
|
47
|
+
/** Lists candidate pointers. Unreadable or malformed entries are skipped, never guessed at. */
|
|
48
|
+
export declare function listRestorePointers(): RestorePointer[];
|
|
49
|
+
/**
|
|
50
|
+
* Exact reference to one candidate, for `skc session restore --reference`.
|
|
51
|
+
*
|
|
52
|
+
* base64url of the identity pair, so a reference cannot be confused with a
|
|
53
|
+
* session id, a path, or a prefix. It selects a candidate; it never overrides
|
|
54
|
+
* any eligibility check.
|
|
55
|
+
*/
|
|
56
|
+
export declare function encodeRestoreReference(coordinatorSessionId: string, stateFile: string): string;
|
|
57
|
+
export declare function decodeRestoreReference(reference: string): {
|
|
58
|
+
coordinatorSessionId: string;
|
|
59
|
+
stateFile: string;
|
|
60
|
+
} | null;
|
|
61
|
+
export type RestoreIneligibleReason = "same_boot" | "boot_unknown" | "sidecar_missing" | "sidecar_identity_mismatch" | "sidecar_terminal" | "transcript_missing" | "cwd_missing" | "live_identity_collision" | "transcript_identity_mismatch" | "unsupported_owner_proof";
|
|
62
|
+
export type RestoreCandidateVerdict = {
|
|
63
|
+
eligible: true;
|
|
64
|
+
pointer: RestorePointer;
|
|
65
|
+
} | {
|
|
66
|
+
eligible: false;
|
|
67
|
+
pointer: RestorePointer;
|
|
68
|
+
reason: RestoreIneligibleReason;
|
|
69
|
+
detail?: string;
|
|
70
|
+
};
|
|
71
|
+
/** The sidecar fields restore is allowed to trust, read fresh at decision time. */
|
|
72
|
+
export interface RestoreSidecarFacts {
|
|
73
|
+
sessionId: string;
|
|
74
|
+
stateFile: string;
|
|
75
|
+
sessionFile: string | null;
|
|
76
|
+
cwd: string | null;
|
|
77
|
+
terminal: boolean;
|
|
78
|
+
}
|
|
79
|
+
export interface RestoreCandidateDeps {
|
|
80
|
+
currentBoot: BootGeneration;
|
|
81
|
+
/** Strict re-read of the referenced sidecar. Null when absent or unparseable. */
|
|
82
|
+
readSidecar: (pointer: RestorePointer) => RestoreSidecarFacts | null;
|
|
83
|
+
pathExists: (target: string) => boolean;
|
|
84
|
+
/** True when a live tmux session already owns this identity. */
|
|
85
|
+
hasLiveIdentity: (pointer: RestorePointer) => boolean;
|
|
86
|
+
/** Header id of the transcript the pointer names, re-read at decision time. */
|
|
87
|
+
readTranscriptSessionId: (pointer: RestorePointer) => string | null;
|
|
88
|
+
/** False when this host cannot produce the owner proof restore requires (psmux). */
|
|
89
|
+
ownerProofAvailable: () => boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Decides whether one candidate may be restored.
|
|
93
|
+
*
|
|
94
|
+
* Order matters: the reboot proof comes first because it is the cheapest and the
|
|
95
|
+
* most restrictive gate, and the pointer's own contents are never trusted beyond
|
|
96
|
+
* naming what to re-read.
|
|
97
|
+
*/
|
|
98
|
+
export declare function evaluateRestoreCandidate(pointer: RestorePointer, deps: RestoreCandidateDeps): RestoreCandidateVerdict;
|
|
99
|
+
export declare function evaluateRestoreCandidates(pointers: readonly RestorePointer[], deps: RestoreCandidateDeps): RestoreCandidateVerdict[];
|