@threadbase-sh/streamer 1.36.4 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +27606 -25177
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +2568 -205
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +503 -4
- package/dist/index.d.ts +503 -4
- package/dist/index.js +2570 -208
- package/dist/index.js.map +1 -1
- package/dist/launchd-entry.cjs +113 -30
- package/dist/launchd-entry.cjs.map +1 -1
- package/dist/migrations/010_create_managed_sessions.sql +65 -0
- package/dist/migrations/011_create_devices.sql +39 -0
- package/dist/migrations/012_create_push_tokens.sql +49 -0
- package/dist/migrations/013_add_push_token_kind.sql +63 -0
- package/dist/pg-migrations/007_create_push_tokens.sql +93 -0
- package/package.json +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
-- Managed-session registry (durable session runtime, C1 Phase 2).
|
|
2
|
+
-- See docs/architecture/2026-07-24-durable-session-runtime.md.
|
|
3
|
+
--
|
|
4
|
+
-- Until now managed session state lived only in SessionStore's in-memory Maps,
|
|
5
|
+
-- so a streamer restart lost startedAt, promptCount, sessionName, projectId,
|
|
6
|
+
-- the Codex placeholder→rollout binding, and failureReason outright. Sessions
|
|
7
|
+
-- did not come back as "recoverable" — they ceased to exist, reappearing at
|
|
8
|
+
-- best as external discovered processes with no managed metadata.
|
|
9
|
+
--
|
|
10
|
+
-- This table stores identity and provenance so the boot reconciler can say what
|
|
11
|
+
-- happened to each session. It deliberately does NOT store the byte stream:
|
|
12
|
+
-- outputBuffer is 64KiB of raw ANSI rewritten on every PTY chunk, and its
|
|
13
|
+
-- authoritative copy is already the provider's JSONL. Persisting it would turn
|
|
14
|
+
-- every chunk into a DB write to duplicate data we can re-read. Post-restart
|
|
15
|
+
-- replay is therefore conversation-accurate, not byte-accurate.
|
|
16
|
+
CREATE TABLE IF NOT EXISTS managed_sessions (
|
|
17
|
+
-- Provider-native resume identifier: the JSONL UUID for Claude Code, the
|
|
18
|
+
-- rollout id for Codex. This is what --resume / `codex resume` consumes, so
|
|
19
|
+
-- the registry never invents an identifier of its own.
|
|
20
|
+
session_id TEXT PRIMARY KEY,
|
|
21
|
+
provider TEXT NOT NULL,
|
|
22
|
+
|
|
23
|
+
-- Liveness probing. pid alone is never treated as identity: PIDs get reused,
|
|
24
|
+
-- so the reconciler matches the recorded cmdline before claiming a live
|
|
25
|
+
-- process is ours, and reports `orphaned` on a mismatch rather than guessing.
|
|
26
|
+
pid INTEGER,
|
|
27
|
+
cmdline TEXT,
|
|
28
|
+
|
|
29
|
+
project_path TEXT NOT NULL,
|
|
30
|
+
project_name TEXT NOT NULL,
|
|
31
|
+
branch TEXT NOT NULL DEFAULT '',
|
|
32
|
+
|
|
33
|
+
-- Semantic status (running/waiting_input/idle) as last observed. The
|
|
34
|
+
-- reconciler never trusts this over a live PID probe — a SIGKILLed streamer
|
|
35
|
+
-- never ran its exit writes, so a stored 'running' can be arbitrarily stale.
|
|
36
|
+
-- status_source records how the value was obtained so that staleness is
|
|
37
|
+
-- visible instead of implied.
|
|
38
|
+
status TEXT NOT NULL,
|
|
39
|
+
status_source TEXT NOT NULL,
|
|
40
|
+
status_updated_at INTEGER NOT NULL,
|
|
41
|
+
|
|
42
|
+
started_at INTEGER NOT NULL,
|
|
43
|
+
completed_at INTEGER,
|
|
44
|
+
last_activity_at INTEGER,
|
|
45
|
+
prompt_count INTEGER NOT NULL DEFAULT 0,
|
|
46
|
+
|
|
47
|
+
-- User-visible identity that today vanishes silently on restart.
|
|
48
|
+
session_name TEXT,
|
|
49
|
+
project_id TEXT,
|
|
50
|
+
-- Codex two-id model: the placeholder id the session was created under vs the
|
|
51
|
+
-- rollout id its history is indexed by. Losing this on restart orphans the
|
|
52
|
+
-- conversation from the session (types.ts:36-44).
|
|
53
|
+
bound_conversation_id TEXT,
|
|
54
|
+
resumed_from_conversation_id TEXT,
|
|
55
|
+
failure_reason TEXT,
|
|
56
|
+
|
|
57
|
+
-- Which streamer run started this session. A row whose instance differs from
|
|
58
|
+
-- the current run is the orphan test: the process outlived its streamer.
|
|
59
|
+
streamer_instance_id TEXT NOT NULL
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
-- The reconciler's only read path on boot: every row not already in a terminal
|
|
63
|
+
-- state. Terminal rows are retained for history but never re-probed.
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_managed_sessions_status
|
|
65
|
+
ON managed_sessions (status);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
-- Paired-device registry (C5 scoped device capabilities).
|
|
2
|
+
-- See docs/architecture/2026-07-24-device-identity-and-capabilities.md.
|
|
3
|
+
--
|
|
4
|
+
-- Pairing exchanged a short-lived token for the streamer's API key — the SAME
|
|
5
|
+
-- string for every device that ever paired. Nothing recorded that a device
|
|
6
|
+
-- existed, so there was no attribution, no per-device revocation (rotating the
|
|
7
|
+
-- key de-authenticated everyone at once), and no way to scope authority.
|
|
8
|
+
CREATE TABLE IF NOT EXISTS devices (
|
|
9
|
+
-- Minted server-side. A client-supplied id would let one device claim
|
|
10
|
+
-- another's identity.
|
|
11
|
+
device_id TEXT PRIMARY KEY,
|
|
12
|
+
|
|
13
|
+
-- The client public key already supplied at pairing, previously used once as
|
|
14
|
+
-- a sealing target and then discarded.
|
|
15
|
+
public_key TEXT NOT NULL,
|
|
16
|
+
|
|
17
|
+
-- SHA-256 of the device token, never the token itself: a read of this table
|
|
18
|
+
-- must not let anyone impersonate a device. Same reasoning as password hashes.
|
|
19
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
20
|
+
|
|
21
|
+
-- Client-supplied label ("Ronen's iPhone"). Display only, never trusted for
|
|
22
|
+
-- authorization.
|
|
23
|
+
name TEXT,
|
|
24
|
+
|
|
25
|
+
-- JSON array of capability strings. Unknown entries are dropped on read, so a
|
|
26
|
+
-- downgrade cannot silently grant a capability this build does not understand.
|
|
27
|
+
capabilities TEXT NOT NULL,
|
|
28
|
+
|
|
29
|
+
created_at INTEGER NOT NULL,
|
|
30
|
+
last_seen_at INTEGER,
|
|
31
|
+
|
|
32
|
+
-- Set to revoke. Checked per request rather than cached — a stale cache is
|
|
33
|
+
-- exactly the window that makes revocation useless.
|
|
34
|
+
revoked_at INTEGER
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
-- Authentication looks a device up by token hash on every request, so this is
|
|
38
|
+
-- the hot path.
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_devices_token_hash ON devices (token_hash);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
-- Push registration and delivery state (C7 notification reliability).
|
|
2
|
+
--
|
|
3
|
+
-- POST /api/push/register was a no-op returning { ok: true }: mobile registered,
|
|
4
|
+
-- received success, and nothing was stored. No token existed, so no notification
|
|
5
|
+
-- could ever be delivered, no failure could be observed, and the client had no
|
|
6
|
+
-- way to discover that its "successful" registration meant nothing.
|
|
7
|
+
CREATE TABLE IF NOT EXISTS push_tokens (
|
|
8
|
+
-- The provider push token (Expo). Natural key: re-registering the same token
|
|
9
|
+
-- must update the existing row rather than accumulate duplicates, which is
|
|
10
|
+
-- how the same device ends up receiving one notification several times.
|
|
11
|
+
token TEXT PRIMARY KEY,
|
|
12
|
+
platform TEXT NOT NULL,
|
|
13
|
+
|
|
14
|
+
-- Optional device attribution. Nullable because push registration predates
|
|
15
|
+
-- device identity (C5) and must keep working without it.
|
|
16
|
+
device_id TEXT,
|
|
17
|
+
|
|
18
|
+
registered_at INTEGER NOT NULL,
|
|
19
|
+
|
|
20
|
+
-- Delivery health. Distinguishing "we have never tried" from "we tried and it
|
|
21
|
+
-- failed" is the difference between a client showing "not yet delivered" and
|
|
22
|
+
-- "your notifications are broken" — the report the user actually needs.
|
|
23
|
+
last_success_at INTEGER,
|
|
24
|
+
last_failure_at INTEGER,
|
|
25
|
+
last_failure_code TEXT,
|
|
26
|
+
|
|
27
|
+
-- Consecutive failures. Reset on success. A token the provider has rejected
|
|
28
|
+
-- repeatedly is dead (app uninstalled, token rotated) and should stop being
|
|
29
|
+
-- retried rather than failing forever.
|
|
30
|
+
failure_streak INTEGER NOT NULL DEFAULT 0,
|
|
31
|
+
|
|
32
|
+
-- Set when the provider tells us the token is permanently invalid, or the
|
|
33
|
+
-- user unregisters. Retained rather than deleted so the health report can
|
|
34
|
+
-- explain why delivery stopped.
|
|
35
|
+
revoked_at INTEGER
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
-- Delivery attempts are keyed by event id so a retry, a reconnect
|
|
39
|
+
-- reconciliation, or a duplicate trigger cannot notify the user twice for the
|
|
40
|
+
-- same underlying event.
|
|
41
|
+
CREATE TABLE IF NOT EXISTS push_events (
|
|
42
|
+
event_id TEXT PRIMARY KEY,
|
|
43
|
+
session_id TEXT,
|
|
44
|
+
created_at INTEGER NOT NULL,
|
|
45
|
+
delivered_at INTEGER
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_push_tokens_revoked ON push_tokens (revoked_at);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_push_events_created ON push_events (created_at);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
-- Push token kinds, for iOS Live Activities (Feature 12 phase 1b).
|
|
2
|
+
--
|
|
3
|
+
-- push_tokens (012) modelled exactly one token per device: the Expo relay token
|
|
4
|
+
-- used for ordinary notifications. A device now supplies three distinct token
|
|
5
|
+
-- types, and they are NOT interchangeable:
|
|
6
|
+
--
|
|
7
|
+
-- expo — Expo relay token. Ordinary push notifications.
|
|
8
|
+
-- liveactivity_start — ActivityKit push-to-start token. App-wide, one per
|
|
9
|
+
-- device, long-lived. Starts an activity when none exists.
|
|
10
|
+
-- liveactivity_update — ActivityKit per-activity update token. Issued by iOS
|
|
11
|
+
-- AFTER an activity starts, scoped to that one activity,
|
|
12
|
+
-- and short-lived.
|
|
13
|
+
--
|
|
14
|
+
-- Conflating them silently breaks delivery: an ActivityKit token posted to
|
|
15
|
+
-- Expo's relay is rejected, and an Expo token used to sign a
|
|
16
|
+
-- `.push-type.liveactivity` topic is rejected by APNs. Neither failure is
|
|
17
|
+
-- visible at registration time, which is exactly why kind is stored explicitly
|
|
18
|
+
-- rather than inferred from token shape.
|
|
19
|
+
ALTER TABLE push_tokens ADD COLUMN kind TEXT NOT NULL DEFAULT 'expo';
|
|
20
|
+
|
|
21
|
+
-- The ActivityKit activity this token updates, for kind =
|
|
22
|
+
-- 'liveactivity_update' only. A device runs several activities at once (one per
|
|
23
|
+
-- live session), so device_id alone cannot identify which row to update or end
|
|
24
|
+
-- — the reason 012's one-row-per-device shape does not stretch to cover this.
|
|
25
|
+
ALTER TABLE push_tokens ADD COLUMN activity_id TEXT;
|
|
26
|
+
|
|
27
|
+
-- The session this activity is rendering. Sending is driven by session
|
|
28
|
+
-- lifecycle transitions, so the send path looks tokens up by session, not by
|
|
29
|
+
-- device.
|
|
30
|
+
ALTER TABLE push_tokens ADD COLUMN session_id TEXT;
|
|
31
|
+
|
|
32
|
+
-- Per-activity tokens expire. Retained rather than deleted (matching
|
|
33
|
+
-- revoked_at's reasoning in 012) so the health report can say "expired"
|
|
34
|
+
-- instead of the row simply vanishing.
|
|
35
|
+
ALTER TABLE push_tokens ADD COLUMN expires_at INTEGER;
|
|
36
|
+
|
|
37
|
+
-- iOS ends a Live Activity ~8h after it starts, so a long session must be
|
|
38
|
+
-- renewed before the cap. Stored per activity rather than derived at read time:
|
|
39
|
+
-- a renewal must survive a restart, and re-arming timers on boot needs a
|
|
40
|
+
-- persisted deadline to read (an in-process timer does not survive).
|
|
41
|
+
ALTER TABLE push_tokens ADD COLUMN stale_date INTEGER;
|
|
42
|
+
|
|
43
|
+
-- The activity's ORIGINAL start, carried unchanged across every renewal.
|
|
44
|
+
-- iOS renders its own ticking elapsed timer from this value, so a renewal that
|
|
45
|
+
-- stamps a fresh start visibly resets the user's timer to zero. Persisting the
|
|
46
|
+
-- original is what makes elapsed time continuous across a renewal.
|
|
47
|
+
ALTER TABLE push_tokens ADD COLUMN started_at INTEGER;
|
|
48
|
+
|
|
49
|
+
-- Set once a renewal has been performed for this row, making renewal
|
|
50
|
+
-- idempotent: a restart mid-window re-arms the timer, and this column is what
|
|
51
|
+
-- stops the re-armed timer from sending a second time.
|
|
52
|
+
ALTER TABLE push_tokens ADD COLUMN renewed_at INTEGER;
|
|
53
|
+
|
|
54
|
+
-- The send path selects by (kind, session) — driven by a session status change,
|
|
55
|
+
-- never by scanning every token.
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_push_tokens_kind_session ON push_tokens (kind, session_id);
|
|
57
|
+
|
|
58
|
+
-- The renewal scheduler scans for activities approaching their cap. Partial
|
|
59
|
+
-- index: only unrenewed rows with a deadline are ever candidates, which keeps
|
|
60
|
+
-- the boot-time re-arm scan proportional to pending renewals rather than to
|
|
61
|
+
-- every token ever registered.
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_push_tokens_stale_date ON push_tokens (stale_date)
|
|
63
|
+
WHERE stale_date IS NOT NULL AND renewed_at IS NULL;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
-- Push registration and delivery state, mirroring SQLite migrations 012 + 013.
|
|
2
|
+
--
|
|
3
|
+
-- SQLite is the primary persistence layer; Postgres is dormant (see CLAUDE.md).
|
|
4
|
+
-- The two schemas are kept in sync so enabling Postgres later does not require
|
|
5
|
+
-- reconstructing history. SQLite reached this shape in two steps (012 created
|
|
6
|
+
-- the table, 013 added the kind columns); Postgres never had 012, so this
|
|
7
|
+
-- single migration creates the end state directly.
|
|
8
|
+
CREATE TABLE IF NOT EXISTS push_tokens (
|
|
9
|
+
-- The provider push token. Natural key: re-registering the same token must
|
|
10
|
+
-- update the existing row rather than accumulate duplicates, which is how one
|
|
11
|
+
-- device ends up receiving the same notification several times.
|
|
12
|
+
token TEXT PRIMARY KEY,
|
|
13
|
+
platform TEXT NOT NULL,
|
|
14
|
+
|
|
15
|
+
-- Optional device attribution. Nullable because push registration predates
|
|
16
|
+
-- device identity (C5) and must keep working without it.
|
|
17
|
+
device_id TEXT,
|
|
18
|
+
|
|
19
|
+
registered_at BIGINT NOT NULL,
|
|
20
|
+
|
|
21
|
+
-- Delivery health. Distinguishing "never tried" from "tried and failed" is
|
|
22
|
+
-- the difference between "not yet delivered" and "your notifications are
|
|
23
|
+
-- broken" — the report the user actually needs.
|
|
24
|
+
last_success_at BIGINT,
|
|
25
|
+
last_failure_at BIGINT,
|
|
26
|
+
last_failure_code TEXT,
|
|
27
|
+
|
|
28
|
+
-- Consecutive failures, reset on success. A token the provider has rejected
|
|
29
|
+
-- repeatedly is dead (app uninstalled, token rotated) and should stop being
|
|
30
|
+
-- retried rather than failing forever.
|
|
31
|
+
failure_streak INTEGER NOT NULL DEFAULT 0,
|
|
32
|
+
|
|
33
|
+
-- Set when the provider reports the token permanently invalid, or the user
|
|
34
|
+
-- unregisters. Retained rather than deleted so the health report can explain
|
|
35
|
+
-- why delivery stopped.
|
|
36
|
+
revoked_at BIGINT,
|
|
37
|
+
|
|
38
|
+
-- Token kind. A device supplies three non-interchangeable types: the Expo
|
|
39
|
+
-- relay token, the ActivityKit push-to-start token (app-wide), and an
|
|
40
|
+
-- ActivityKit per-activity update token (short-lived, issued after an
|
|
41
|
+
-- activity starts). Stored explicitly rather than inferred from token shape,
|
|
42
|
+
-- because a mismatch is rejected by the provider at send time with no signal
|
|
43
|
+
-- at registration time.
|
|
44
|
+
kind TEXT NOT NULL DEFAULT 'expo',
|
|
45
|
+
|
|
46
|
+
-- The ActivityKit activity this token updates (kind =
|
|
47
|
+
-- 'liveactivity_update'). A device runs several activities at once, one per
|
|
48
|
+
-- live session, so device_id alone cannot identify which row to update.
|
|
49
|
+
activity_id TEXT,
|
|
50
|
+
|
|
51
|
+
-- The session this activity renders. Sending is driven by session lifecycle
|
|
52
|
+
-- transitions, so the send path looks up by session rather than by device.
|
|
53
|
+
session_id TEXT,
|
|
54
|
+
|
|
55
|
+
-- Per-activity tokens expire. Retained rather than deleted so health can
|
|
56
|
+
-- report "expired" instead of the row vanishing.
|
|
57
|
+
expires_at BIGINT,
|
|
58
|
+
|
|
59
|
+
-- iOS ends a Live Activity ~8h after it starts. Persisted per activity
|
|
60
|
+
-- because a renewal must survive a restart: re-arming timers on boot needs a
|
|
61
|
+
-- durable deadline to read.
|
|
62
|
+
stale_date BIGINT,
|
|
63
|
+
|
|
64
|
+
-- The activity's ORIGINAL start, carried unchanged across every renewal. iOS
|
|
65
|
+
-- renders its own ticking timer from this, so a renewal stamping a fresh
|
|
66
|
+
-- start visibly resets the user's elapsed time to zero.
|
|
67
|
+
started_at BIGINT,
|
|
68
|
+
|
|
69
|
+
-- Set once this row has been renewed, making renewal idempotent: a restart
|
|
70
|
+
-- mid-window re-arms the timer, and this is what stops a second send.
|
|
71
|
+
renewed_at BIGINT
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
-- Delivery attempts keyed by event id so a retry, a reconnect reconciliation,
|
|
75
|
+
-- or a duplicate trigger cannot notify the user twice for one event.
|
|
76
|
+
CREATE TABLE IF NOT EXISTS push_events (
|
|
77
|
+
event_id TEXT PRIMARY KEY,
|
|
78
|
+
session_id TEXT,
|
|
79
|
+
created_at BIGINT NOT NULL,
|
|
80
|
+
delivered_at BIGINT
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_push_tokens_revoked ON push_tokens (revoked_at);
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_push_events_created ON push_events (created_at);
|
|
85
|
+
|
|
86
|
+
-- The send path selects by (kind, session), driven by a status change rather
|
|
87
|
+
-- than by scanning every token.
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_push_tokens_kind_session ON push_tokens (kind, session_id);
|
|
89
|
+
|
|
90
|
+
-- Renewal candidates only. Partial index keeps the boot-time re-arm scan
|
|
91
|
+
-- proportional to pending renewals, not to every token ever registered.
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_push_tokens_stale_date ON push_tokens (stale_date)
|
|
93
|
+
WHERE stale_date IS NOT NULL AND renewed_at IS NULL;
|
package/package.json
CHANGED