thinkpool-pair 0.7.30 → 0.7.31
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/bridge.mjs +33 -9
- package/event-id.mjs +40 -0
- package/package.json +1 -1
package/bridge.mjs
CHANGED
|
@@ -39,7 +39,7 @@ import { createClient } from '@supabase/supabase-js'
|
|
|
39
39
|
import { startClaudeSession } from './claude-session.mjs'
|
|
40
40
|
import { turnInFlight } from './update-gate.mjs'
|
|
41
41
|
import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames } from './session-store.mjs'
|
|
42
|
-
import { stampEvent } from './event-id.mjs'
|
|
42
|
+
import { stampEvent, makeSeqCounter, maxSeq, seqable } from './event-id.mjs'
|
|
43
43
|
|
|
44
44
|
// Public client creds (the same anon values the web app ships — safe to embed).
|
|
45
45
|
// Override with TP_SUPABASE_URL / TP_SUPABASE_ANON if you ever need to.
|
|
@@ -615,6 +615,20 @@ function printLocal(evt) {
|
|
|
615
615
|
}
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
+
// Single stamp+append point for a structured session's replayed log. Assigns the
|
|
619
|
+
// per-term contiguous seq (C1), appends, and caps at STRUCTURED_LOG_MAX (oldest
|
|
620
|
+
// first). EVERY event that joins entry.log must go through here so the client's
|
|
621
|
+
// "drop ≤ seqHi" cursor stays correct (a logged event with no seq would break it).
|
|
622
|
+
function pushLog(entry, evt) {
|
|
623
|
+
// Only TRANSCRIPT events consume a seq (seqable) — replay-chrome kinds the web
|
|
624
|
+
// filters out (system/thinking_tokens/effort) ride the log seq-less so they don't
|
|
625
|
+
// hole the client's applied sequence. See event-id.mjs NO_SEQ_KINDS.
|
|
626
|
+
if (entry.seq && seqable(evt)) evt.seq = entry.seq.next()
|
|
627
|
+
entry.log.push(evt)
|
|
628
|
+
if (entry.log.length > STRUCTURED_LOG_MAX) entry.log.shift()
|
|
629
|
+
return evt
|
|
630
|
+
}
|
|
631
|
+
|
|
618
632
|
// The Phase-2 path: instead of a PTY, run Claude Code through the Agent SDK and
|
|
619
633
|
// relay STRUCTURED events. onEvent → broadcast `code-event` + print locally +
|
|
620
634
|
// persist to the host file; tool calls round-trip through the perm card; the
|
|
@@ -622,6 +636,11 @@ function printLocal(evt) {
|
|
|
622
636
|
function openStructured({ id, model, resume, log, commands, mode }) {
|
|
623
637
|
if (sessions.has(id)) return
|
|
624
638
|
const entry = { cmd: 'claude', kind: 'structured', log: Array.isArray(log) ? log.slice(-STRUCTURED_LOG_MAX) : [], pending: new Map(), session: null, recovered: false, commands: Array.isArray(commands) ? commands : undefined, mode }
|
|
639
|
+
// C1 (RT-2): per-term contiguous seq counter. Seeded from the restored log's max
|
|
640
|
+
// so a bridge restart resumes ABOVE every persisted seq — fresh events never reuse
|
|
641
|
+
// an old seq, which is what keeps the between-turns restart dup-free (see
|
|
642
|
+
// event-id.mjs + src/pages/code/seqDedup.js). pushLog() is the single stamp point.
|
|
643
|
+
entry.seq = makeSeqCounter(maxSeq(entry.log))
|
|
625
644
|
sessions.set(id, entry)
|
|
626
645
|
// This session's private mockup outbox — render.sh writes manifests here (via
|
|
627
646
|
// TP_MOCKUP_OUTBOX below) and the watcher attributes every card to THIS id, so
|
|
@@ -671,10 +690,7 @@ function openStructured({ id, model, resume, log, commands, mode }) {
|
|
|
671
690
|
entry.compacting = false
|
|
672
691
|
bcast('code-event', { term: id, evt: { kind: 'compact', status: 'done', ts: Date.now() } })
|
|
673
692
|
}
|
|
674
|
-
if (!chrome)
|
|
675
|
-
entry.log.push(evt)
|
|
676
|
-
if (entry.log.length > STRUCTURED_LOG_MAX) entry.log.shift()
|
|
677
|
-
}
|
|
693
|
+
if (!chrome) pushLog(entry, evt)
|
|
678
694
|
bcast('code-event', { term: id, evt })
|
|
679
695
|
printLocal(evt)
|
|
680
696
|
if (!chrome) persist()
|
|
@@ -831,9 +847,17 @@ channel
|
|
|
831
847
|
})
|
|
832
848
|
}
|
|
833
849
|
// Structured sessions replay their event log (reader rebuilds from it).
|
|
850
|
+
// C1 (RT-2): send only the tail past the client's per-term cursor (seqHi) when
|
|
851
|
+
// it supplies one — the authoritative replay-from-cursor that supersedes
|
|
852
|
+
// re-sending + re-merging the whole log on every reconnect. Events with no seq
|
|
853
|
+
// (legacy/pre-C1 logs) ride along always; the client cid-dedups those and
|
|
854
|
+
// additionally drops ≤ seqHi, so an over-send is harmless. A fresh joiner (no
|
|
855
|
+
// cursor) still gets the full log.
|
|
834
856
|
for (const [id, s] of sessions) {
|
|
835
857
|
if (!s.log.length) continue
|
|
836
|
-
|
|
858
|
+
const from = Number(payload?.cursors?.[id]) || 0
|
|
859
|
+
const events = from > 0 ? s.log.filter((e) => e.seq == null || e.seq > from) : s.log
|
|
860
|
+
bcast('code-replay', { to: payload?.to ?? null, term: id, events })
|
|
837
861
|
}
|
|
838
862
|
// Re-send any still-pending permission/question cards. They ride a one-shot
|
|
839
863
|
// code-perm-req broadcast (NOT the replayed event log), so a reconnect/refresh
|
|
@@ -881,7 +905,7 @@ channel
|
|
|
881
905
|
const echoYou = () => {
|
|
882
906
|
if (payload.silent) return
|
|
883
907
|
const evt = { kind: 'you', text, cid: payload.cid, by: payload.by }
|
|
884
|
-
|
|
908
|
+
pushLog(s, evt)
|
|
885
909
|
bcast('code-event', { term: payload.term, evt })
|
|
886
910
|
}
|
|
887
911
|
// Session controls (/model, /clear) are NOT agent turns — they change the
|
|
@@ -891,7 +915,7 @@ channel
|
|
|
891
915
|
// slash still flow through as a real, self-terminating turn.
|
|
892
916
|
const ctlLine = (ctlText) => {
|
|
893
917
|
const evt = { kind: 'control', text: ctlText, by: payload.by, cid: payload.cid }
|
|
894
|
-
|
|
918
|
+
pushLog(s, evt)
|
|
895
919
|
bcast('code-event', { term: payload.term, evt })
|
|
896
920
|
}
|
|
897
921
|
const mm = text.match(/^\/model\b\s*(\S+)?/)
|
|
@@ -902,7 +926,7 @@ channel
|
|
|
902
926
|
}
|
|
903
927
|
if (/^\/clear\s*$/.test(text)) {
|
|
904
928
|
s.session.sendTurn(text)
|
|
905
|
-
s.log = []; bcast('code-event', { term: payload.term, evt: { kind: 'clear' } })
|
|
929
|
+
s.log = []; s.seq = makeSeqCounter(0); bcast('code-event', { term: payload.term, evt: { kind: 'clear' } })
|
|
906
930
|
flushSession(room, payload.term, { sessionId: s.session?.sessionId || null, log: [] })
|
|
907
931
|
ctlLine('context cleared. You can continue with these answers in mind.')
|
|
908
932
|
return
|
package/event-id.mjs
CHANGED
|
@@ -27,3 +27,43 @@ export function stampEvent(evt, gen = randomUUID) {
|
|
|
27
27
|
if (!evt.cid) evt.cid = gen()
|
|
28
28
|
return evt
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
/* C1 (RT-2): a per-term monotonic, CONTIGUOUS sequence counter. Every event that
|
|
32
|
+
enters a session's replayed log gets `seq = counter.next()`, so the web room can
|
|
33
|
+
replay-from-cursor and dedup by "drop ≤ seqHi" instead of re-merging the whole
|
|
34
|
+
log by cid on every reconnect (see src/pages/code/seqDedup.js).
|
|
35
|
+
|
|
36
|
+
THE restart property (what makes Slice 3's between-turns restart dup-free): when a
|
|
37
|
+
bridge restart restores a persisted log, seed the counter from maxSeq(log) so the
|
|
38
|
+
NEXT seq is strictly above every restored seq. Reusing an old seq would make the
|
|
39
|
+
client drop the fresh event as a duplicate. */
|
|
40
|
+
export function makeSeqCounter(startAfter = 0) {
|
|
41
|
+
let n = Number.isFinite(startAfter) ? startAfter : 0
|
|
42
|
+
return {
|
|
43
|
+
next: () => ++n,
|
|
44
|
+
bumpTo: (m) => { if (Number.isFinite(m) && m > n) n = m },
|
|
45
|
+
current: () => n,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Kinds the bridge logs for the live transcript but that the WEB treats as
|
|
50
|
+
// replay-chrome (src/pages/code/code-constants.js REPLAY_CHROME): filtered out of
|
|
51
|
+
// the replay + handled as transient state, never applied as a transcript line. They
|
|
52
|
+
// must NOT consume a per-term seq — the client advances its cursor (seqHi) ONLY over
|
|
53
|
+
// transcript events, so a seq on one of these would leave a hole in the applied
|
|
54
|
+
// sequence → the client sees a false gap → spurious resync storms. So the counter
|
|
55
|
+
// skips them and they ride the log seq-less (the client cid-handles/ignores them).
|
|
56
|
+
// MUST stay a subset of REPLAY_CHROME ∩ {kinds the bridge logs} — guarded by
|
|
57
|
+
// bridge/test-seq.mjs S6. (mode/usage/clear/compact/stalled are bridge-chrome and
|
|
58
|
+
// never logged at all, so they never reach the counter.)
|
|
59
|
+
export const NO_SEQ_KINDS = new Set(['system', 'thinking_tokens', 'effort'])
|
|
60
|
+
export const seqable = (evt) => !!evt && typeof evt === 'object' && !NO_SEQ_KINDS.has(evt.kind)
|
|
61
|
+
|
|
62
|
+
// Highest numeric `seq` across a log of events (0 when none) — the floor a restored
|
|
63
|
+
// session's counter resumes above.
|
|
64
|
+
export function maxSeq(events) {
|
|
65
|
+
if (!Array.isArray(events)) return 0
|
|
66
|
+
let m = 0
|
|
67
|
+
for (const e of events) if (e && typeof e.seq === 'number' && e.seq > m) m = e.seq
|
|
68
|
+
return m
|
|
69
|
+
}
|