@evandu/super-pi 0.1.145 → 0.1.147
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/agents/super-pi-task/core.ts +74 -2
- package/agents/super-pi-task/index.ts +7 -0
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +11 -11
- package/dist/client/assets/{CodeMirrorEditor-A9KvbIkm.js → CodeMirrorEditor-DQCjEnxG.js} +2 -2
- package/dist/client/assets/{dist-DM4Olwfx.js → dist-1Innu17E.js} +1 -1
- package/dist/client/assets/{dist-bIDce5j-.js → dist-1lynFOc5.js} +1 -1
- package/dist/client/assets/{dist-BlxwDnWr.js → dist-B1qLZVMK.js} +1 -1
- package/dist/client/assets/{dist-BTVbZigv.js → dist-B4pz_xdd.js} +1 -1
- package/dist/client/assets/{dist-UJ46aPS3.js → dist-B6KwDpeI.js} +1 -1
- package/dist/client/assets/{dist-CK2KFFtT.js → dist-BJVI1Unv.js} +1 -1
- package/dist/client/assets/{dist-CRnm1rnO.js → dist-BWvzD8Pe.js} +1 -1
- package/dist/client/assets/{dist-DSHM-PAs.js → dist-BayWssd0.js} +1 -1
- package/dist/client/assets/{dist-B9DE7sc5.js → dist-Bk_pkBVH.js} +1 -1
- package/dist/client/assets/{dist-D-o7iQ8c.js → dist-BpeClAv0.js} +1 -1
- package/dist/client/assets/{dist-CQFASxyb.js → dist-BsmJg5PX.js} +1 -1
- package/dist/client/assets/{dist-Ds5gfeAi.js → dist-Bx7s4rni.js} +1 -1
- package/dist/client/assets/{dist-UPJPMjWv.js → dist-CDBKM9tL.js} +1 -1
- package/dist/client/assets/{dist-CIgjifH-.js → dist-CGzXCzCG.js} +1 -1
- package/dist/client/assets/{dist-BAiV8I_O.js → dist-CRxCwn-Z.js} +1 -1
- package/dist/client/assets/{dist-Br-AuQUG.js → dist-CTDgUnBG.js} +1 -1
- package/dist/client/assets/{dist-4JbFTwg2.js → dist-D1VfMWmf.js} +1 -1
- package/dist/client/assets/{dist-BYOpvW02.js → dist-DTZLNEci.js} +1 -1
- package/dist/client/assets/{dist-CQV5kOCE.js → dist-DuUi29km.js} +1 -1
- package/dist/client/assets/{dist-CjszAoXk.js → dist-F3S0t1oM.js} +1 -1
- package/dist/client/assets/{dist-2boAf_pN.js → dist-QZ-FAVJV.js} +1 -1
- package/dist/client/assets/{dist-B1p4YF9C.js → dist-u0nBg0fZ.js} +1 -1
- package/dist/client/assets/index-BMpqviwU.css +1 -0
- package/dist/client/assets/index-Dzyp_7sf.js +102 -0
- package/dist/client/index.html +2 -2
- package/dist/server/active-memory.d.ts +17 -0
- package/dist/server/active-memory.js +39 -0
- package/dist/server/agents.d.ts +1 -1
- package/dist/server/agents.js +3 -1
- package/dist/server/application/conversation-service.d.ts +1 -1
- package/dist/server/application/conversation-service.js +12 -4
- package/dist/server/application/run-service.d.ts +32 -2
- package/dist/server/application/run-service.js +296 -40
- package/dist/server/application/scheduled-task-service.d.ts +14 -1
- package/dist/server/application/scheduled-task-service.js +27 -2
- package/dist/server/application/session-orchestrator-service.d.ts +114 -3
- package/dist/server/application/session-orchestrator-service.js +406 -35
- package/dist/server/application/session-watchdog.d.ts +159 -0
- package/dist/server/application/session-watchdog.js +244 -0
- package/dist/server/command-handler.js +1 -1
- package/dist/server/conversation-usage.d.ts +19 -0
- package/dist/server/conversation-usage.js +40 -0
- package/dist/server/file-diff.d.ts +3 -0
- package/dist/server/file-diff.js +4 -4
- package/dist/server/file-index.d.ts +4 -2
- package/dist/server/file-index.js +14 -9
- package/dist/server/file-name-index.d.ts +5 -2
- package/dist/server/file-name-index.js +22 -5
- package/dist/server/git.d.ts +3 -3
- package/dist/server/git.js +10 -9
- package/dist/server/http-server.js +256 -34
- package/dist/server/index.js +3 -0
- package/dist/server/knowledge-vectors.d.ts +15 -0
- package/dist/server/knowledge-vectors.js +118 -37
- package/dist/server/knowledge.d.ts +7 -8
- package/dist/server/knowledge.js +133 -55
- package/dist/server/length-continuation.d.ts +50 -0
- package/dist/server/length-continuation.js +55 -0
- package/dist/server/model-context-window.d.ts +46 -0
- package/dist/server/model-context-window.js +121 -0
- package/dist/server/pi-session-events.js +5 -1
- package/dist/server/pi-session-pool.d.ts +2 -0
- package/dist/server/pi-session-pool.js +16 -0
- package/dist/server/pi-session.d.ts +6 -0
- package/dist/server/pi-session.js +15 -1
- package/dist/server/runtime.d.ts +3 -0
- package/dist/server/runtime.js +23 -7
- package/dist/server/session-cwd.d.ts +17 -0
- package/dist/server/session-cwd.js +43 -0
- package/dist/server/session-ledger.d.ts +7 -0
- package/dist/server/session-tree-scope.d.ts +125 -0
- package/dist/server/session-tree-scope.js +337 -0
- package/dist/server/store.d.ts +35 -0
- package/dist/server/store.js +87 -2
- package/dist/server/task-tool.d.ts +2 -1
- package/dist/server/task-tool.js +4 -1
- package/dist/server/terminal.d.ts +1 -0
- package/dist/server/terminal.js +5 -3
- package/dist/server/turn-failure.d.ts +26 -0
- package/dist/server/turn-failure.js +67 -1
- package/dist/server/types.d.ts +33 -1
- package/dist/server/version.d.ts +25 -2
- package/dist/server/version.js +98 -26
- package/dist/server/ws-server.d.ts +1 -1
- package/dist/server/ws-server.js +7 -5
- package/package.json +36 -3
- package/dist/client/assets/index-BGsP8pt_.js +0 -100
- package/dist/client/assets/index-CiIc734l.css +0 -1
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE WATCHDOG: A TIMER THAT SAYS "THE CHILD WENT QUIET".
|
|
3
|
+
*
|
|
4
|
+
* WHAT IT IS. A parent session declares, for one child session, how long it is willing to wait for a
|
|
5
|
+
* report before it wants to be told. When that silence runs out, the parent receives ONE message - a
|
|
6
|
+
* reminder - and the clock starts again. That is the whole contract.
|
|
7
|
+
*
|
|
8
|
+
* WHAT IT IS NOT. It has no power over the child. It does not stop the run, does not cancel a queue,
|
|
9
|
+
* does not send anything to the child. Interrupting is the PARENT's decision, and the parent has the
|
|
10
|
+
* tools for it (`session_stop`, `session_dispatch`). A watchdog that killed the run would take away the
|
|
11
|
+
* one judgement the user asked to keep: "父节点可以接受暂时不回报,然后让 watchdog 下一次再反馈".
|
|
12
|
+
*
|
|
13
|
+
* WHY THE REPORT ARRIVES AS A MESSAGE AND NOT AS AN EVENT. The parent is a conversation, and a
|
|
14
|
+
* conversation only knows something when a message says so. A watchdog that pushed a state change would
|
|
15
|
+
* leave the parent's model uninformed - it would keep waiting for a report that was never going to come,
|
|
16
|
+
* because the child had simply decided not to send one.
|
|
17
|
+
*
|
|
18
|
+
* WHY THE STATE IS ON DISK. The parent declared an expectation ("I want to hear from this child within
|
|
19
|
+
* five minutes"). An expectation that evaporates when the server restarts is worse than no watchdog at
|
|
20
|
+
* all: the parent believes it is being watched, and it is not. So the state lives in the workspace, next
|
|
21
|
+
* to the other `.super-pi/` artefacts, and the timers are re-armed when the server comes back.
|
|
22
|
+
*/
|
|
23
|
+
/** One watchdog: the parent that declared it, the child it watches, and the clock. */
|
|
24
|
+
export interface WatchdogEntry {
|
|
25
|
+
/** The child session being watched. */
|
|
26
|
+
childConversationId: string;
|
|
27
|
+
/** The session the reminder is delivered to. */
|
|
28
|
+
parentConversationId: string;
|
|
29
|
+
/** The workspace whose `.super-pi/watchdogs.json` holds this entry. */
|
|
30
|
+
workspaceId: string;
|
|
31
|
+
/** How long a silence lasts before the parent is reminded. */
|
|
32
|
+
timeoutSeconds: number;
|
|
33
|
+
/** When the last sign of life was. A report moves it; the clock runs from here. */
|
|
34
|
+
lastSignalAt: number;
|
|
35
|
+
/** Whether the parent has ALREADY been told that the child is no longer working. */
|
|
36
|
+
endedReminded: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** What the owner of a watchdog is told when the clock runs out. */
|
|
39
|
+
export interface WatchdogTimeout {
|
|
40
|
+
childConversationId: string;
|
|
41
|
+
parentConversationId: string;
|
|
42
|
+
timeoutSeconds: number;
|
|
43
|
+
/** How long the silence actually was, in seconds. */
|
|
44
|
+
elapsedSeconds: number;
|
|
45
|
+
/** Whether the child still has work in flight when the clock ran out. */
|
|
46
|
+
childWorking: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** The file name the state lives under, inside the workspace's `.super-pi` directory. */
|
|
49
|
+
export declare const WATCHDOG_STATE_FILE = "watchdogs.json";
|
|
50
|
+
/** The longest silence a parent may declare: a day. Beyond that it is not a watchdog, it is a plan. */
|
|
51
|
+
export declare const WATCHDOG_MAX_TIMEOUT_SECONDS = 86400;
|
|
52
|
+
/** The shortest silence a parent may declare. One second is a test's value, not a parent's. */
|
|
53
|
+
export declare const WATCHDOG_MIN_TIMEOUT_SECONDS = 1;
|
|
54
|
+
/**
|
|
55
|
+
* What the watchdog needs from the server it lives in.
|
|
56
|
+
*
|
|
57
|
+
* The watchdog knows NOTHING about sessions: it counts seconds and asks its owner to deliver a message.
|
|
58
|
+
* The owner is the session orchestrator, which is the only thing that knows how a message reaches a
|
|
59
|
+
* parent conversation.
|
|
60
|
+
*/
|
|
61
|
+
export interface WatchdogHost {
|
|
62
|
+
/** Delivers the reminder to the parent. */
|
|
63
|
+
onTimeout: (event: WatchdogTimeout) => Promise<void> | void;
|
|
64
|
+
/** The workspace directory a workspace's state file lives in. */
|
|
65
|
+
workspacePath: (workspaceId: string) => string | undefined;
|
|
66
|
+
/** Every workspace id the server knows about. */
|
|
67
|
+
workspaceIds: () => string[];
|
|
68
|
+
/** Whether the child still has work in flight: a live turn, or a queued message waiting for one. */
|
|
69
|
+
isChildWorking: (childConversationId: string) => boolean;
|
|
70
|
+
/** Where complaints go. */
|
|
71
|
+
warn?: (message: string) => void;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The watchdog manager: the timers, the state, and the state file.
|
|
75
|
+
*
|
|
76
|
+
* ONE WATCHDOG PER CHILD. The parent declares a duration for a child, and the duration belongs to that
|
|
77
|
+
* child - not to the pair of sessions in general - because "how long may this child be quiet" is a
|
|
78
|
+
* property of that child, and the parent may well declare different expectations for different children.
|
|
79
|
+
*/
|
|
80
|
+
export declare class SessionWatchdog {
|
|
81
|
+
private readonly host;
|
|
82
|
+
private readonly entries;
|
|
83
|
+
private readonly timers;
|
|
84
|
+
/** One write chain per workspace, so two watchdogs in one workspace cannot interleave a write. */
|
|
85
|
+
private readonly writes;
|
|
86
|
+
constructor(host: WatchdogHost);
|
|
87
|
+
/**
|
|
88
|
+
* Declares (or re-declares) the watchdog for a child.
|
|
89
|
+
*
|
|
90
|
+
* `timeoutSeconds` of 0 CANCELS the watchdog: "I am no longer waiting for a report from this one".
|
|
91
|
+
* That is the only way a parent takes the expectation back, and it is deliberately explicit - a
|
|
92
|
+
* watchdog that quietly switched itself off would leave the parent believing it was still watched.
|
|
93
|
+
*/
|
|
94
|
+
arm(input: {
|
|
95
|
+
childConversationId: string;
|
|
96
|
+
parentConversationId: string;
|
|
97
|
+
workspaceId: string;
|
|
98
|
+
timeoutSeconds: number;
|
|
99
|
+
}): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Records a sign of life: the child reported, so the clock starts again.
|
|
102
|
+
*
|
|
103
|
+
* A REPORT RESETS THE CLOCK, IT DOES NOT CANCEL THE WATCHDOG. The parent asked to hear from this
|
|
104
|
+
* child while the child was working; a report that says "the first stage is done, I am continuing"
|
|
105
|
+
* must not be read as "the work is over". Cancelling on the first report would turn the watchdog into
|
|
106
|
+
* a one-shot device, and the whole point of the parent's declaration is that it holds for as long as
|
|
107
|
+
* the child is out of sight.
|
|
108
|
+
*/
|
|
109
|
+
noteSignal(childConversationId: string): Promise<void>;
|
|
110
|
+
/** Takes the watchdog away. The parent is no longer waiting. */
|
|
111
|
+
cancel(childConversationId: string): Promise<void>;
|
|
112
|
+
/** The watchdogs that are currently armed. */
|
|
113
|
+
list(): WatchdogEntry[];
|
|
114
|
+
/** The watchdog on one child, if there is one. */
|
|
115
|
+
get(childConversationId: string): WatchdogEntry | undefined;
|
|
116
|
+
/**
|
|
117
|
+
* Re-arms the watchdogs that were on disk when the server stopped.
|
|
118
|
+
*
|
|
119
|
+
* A watchdog whose silence had ALREADY run out while the server was down fires as soon as it comes
|
|
120
|
+
* back: the parent asked to be told after N minutes of silence, and the silence did happen. The
|
|
121
|
+
* remaining time is measured from the last sign of life, not from the restart.
|
|
122
|
+
*/
|
|
123
|
+
restore(): Promise<void>;
|
|
124
|
+
/** Stops every timer. The process is going away; the state stays on disk. */
|
|
125
|
+
dispose(): void;
|
|
126
|
+
/**
|
|
127
|
+
* Arms the timer for one entry.
|
|
128
|
+
*
|
|
129
|
+
* THE TIMER IS UNREF'D. A pending reminder must not keep the Node process alive: the server is
|
|
130
|
+
* allowed to shut down with a watchdog in flight, and the state is on disk anyway.
|
|
131
|
+
*/
|
|
132
|
+
private schedule;
|
|
133
|
+
private clearTimer;
|
|
134
|
+
/**
|
|
135
|
+
* The clock ran out.
|
|
136
|
+
*
|
|
137
|
+
* THE CHILD'S STATE DECIDES WHETHER THIS IS A REPEAT, AND THE REPEAT IS NOT LOUDER - IT IS THE
|
|
138
|
+
* SAME. Every interval the parent gets the same reminder, because the parent's need is the same:
|
|
139
|
+
* "I still have no news". An interval that grew would make the watchdog quietest exactly when the
|
|
140
|
+
* child has been silent longest, which is backwards.
|
|
141
|
+
*
|
|
142
|
+
* THE ONE EXCEPTION IS A CHILD THAT IS NO LONGER WORKING. A child whose turn ended without reporting
|
|
143
|
+
* is not going to report: the turn is over. The parent is told once that the child has gone quiet
|
|
144
|
+
* AND stopped, and the watchdog then takes itself off - a reminder that repeats for a child that
|
|
145
|
+
* cannot speak any more is noise, not information.
|
|
146
|
+
*/
|
|
147
|
+
private fire;
|
|
148
|
+
/** The path of one workspace's state file, or undefined when the workspace is unknown. */
|
|
149
|
+
private statePath;
|
|
150
|
+
/**
|
|
151
|
+
* Writes one workspace's state file.
|
|
152
|
+
*
|
|
153
|
+
* THE WRITES ARE SERIALIZED PER WORKSPACE. Two watchdogs in one workspace each rewrite the same
|
|
154
|
+
* file, and an interleaved pair of writes would leave a file that describes one watchdog twice and
|
|
155
|
+
* loses the other.
|
|
156
|
+
*/
|
|
157
|
+
private persist;
|
|
158
|
+
private writeStateFile;
|
|
159
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/** The file name the state lives under, inside the workspace's `.super-pi` directory. */
|
|
4
|
+
export const WATCHDOG_STATE_FILE = "watchdogs.json";
|
|
5
|
+
/** The longest silence a parent may declare: a day. Beyond that it is not a watchdog, it is a plan. */
|
|
6
|
+
export const WATCHDOG_MAX_TIMEOUT_SECONDS = 86_400;
|
|
7
|
+
/** The shortest silence a parent may declare. One second is a test's value, not a parent's. */
|
|
8
|
+
export const WATCHDOG_MIN_TIMEOUT_SECONDS = 1;
|
|
9
|
+
/**
|
|
10
|
+
* The watchdog manager: the timers, the state, and the state file.
|
|
11
|
+
*
|
|
12
|
+
* ONE WATCHDOG PER CHILD. The parent declares a duration for a child, and the duration belongs to that
|
|
13
|
+
* child - not to the pair of sessions in general - because "how long may this child be quiet" is a
|
|
14
|
+
* property of that child, and the parent may well declare different expectations for different children.
|
|
15
|
+
*/
|
|
16
|
+
export class SessionWatchdog {
|
|
17
|
+
host;
|
|
18
|
+
entries = new Map();
|
|
19
|
+
timers = new Map();
|
|
20
|
+
/** One write chain per workspace, so two watchdogs in one workspace cannot interleave a write. */
|
|
21
|
+
writes = new Map();
|
|
22
|
+
constructor(host) {
|
|
23
|
+
this.host = host;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Declares (or re-declares) the watchdog for a child.
|
|
27
|
+
*
|
|
28
|
+
* `timeoutSeconds` of 0 CANCELS the watchdog: "I am no longer waiting for a report from this one".
|
|
29
|
+
* That is the only way a parent takes the expectation back, and it is deliberately explicit - a
|
|
30
|
+
* watchdog that quietly switched itself off would leave the parent believing it was still watched.
|
|
31
|
+
*/
|
|
32
|
+
async arm(input) {
|
|
33
|
+
const { childConversationId } = input;
|
|
34
|
+
if (!childConversationId.trim())
|
|
35
|
+
return;
|
|
36
|
+
this.clearTimer(childConversationId);
|
|
37
|
+
if (!Number.isFinite(input.timeoutSeconds) || input.timeoutSeconds <= 0) {
|
|
38
|
+
// CANCEL. The entry is removed and the file rewritten, so a cancelled watchdog stays cancelled
|
|
39
|
+
// across a restart.
|
|
40
|
+
if (this.entries.delete(childConversationId))
|
|
41
|
+
await this.persist(input.workspaceId);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const timeoutSeconds = Math.min(WATCHDOG_MAX_TIMEOUT_SECONDS, Math.max(WATCHDOG_MIN_TIMEOUT_SECONDS, Math.floor(input.timeoutSeconds)));
|
|
45
|
+
const entry = {
|
|
46
|
+
childConversationId,
|
|
47
|
+
parentConversationId: input.parentConversationId,
|
|
48
|
+
workspaceId: input.workspaceId,
|
|
49
|
+
timeoutSeconds,
|
|
50
|
+
lastSignalAt: Date.now(),
|
|
51
|
+
endedReminded: false,
|
|
52
|
+
};
|
|
53
|
+
this.entries.set(childConversationId, entry);
|
|
54
|
+
await this.persist(input.workspaceId);
|
|
55
|
+
this.schedule(entry);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Records a sign of life: the child reported, so the clock starts again.
|
|
59
|
+
*
|
|
60
|
+
* A REPORT RESETS THE CLOCK, IT DOES NOT CANCEL THE WATCHDOG. The parent asked to hear from this
|
|
61
|
+
* child while the child was working; a report that says "the first stage is done, I am continuing"
|
|
62
|
+
* must not be read as "the work is over". Cancelling on the first report would turn the watchdog into
|
|
63
|
+
* a one-shot device, and the whole point of the parent's declaration is that it holds for as long as
|
|
64
|
+
* the child is out of sight.
|
|
65
|
+
*/
|
|
66
|
+
async noteSignal(childConversationId) {
|
|
67
|
+
const entry = this.entries.get(childConversationId);
|
|
68
|
+
if (!entry)
|
|
69
|
+
return;
|
|
70
|
+
entry.lastSignalAt = Date.now();
|
|
71
|
+
entry.endedReminded = false;
|
|
72
|
+
this.clearTimer(childConversationId);
|
|
73
|
+
this.schedule(entry);
|
|
74
|
+
await this.persist(entry.workspaceId);
|
|
75
|
+
}
|
|
76
|
+
/** Takes the watchdog away. The parent is no longer waiting. */
|
|
77
|
+
async cancel(childConversationId) {
|
|
78
|
+
const entry = this.entries.get(childConversationId);
|
|
79
|
+
this.clearTimer(childConversationId);
|
|
80
|
+
if (!entry)
|
|
81
|
+
return;
|
|
82
|
+
this.entries.delete(childConversationId);
|
|
83
|
+
await this.persist(entry.workspaceId);
|
|
84
|
+
}
|
|
85
|
+
/** The watchdogs that are currently armed. */
|
|
86
|
+
list() {
|
|
87
|
+
return [...this.entries.values()].map((entry) => ({ ...entry }));
|
|
88
|
+
}
|
|
89
|
+
/** The watchdog on one child, if there is one. */
|
|
90
|
+
get(childConversationId) {
|
|
91
|
+
const entry = this.entries.get(childConversationId);
|
|
92
|
+
return entry ? { ...entry } : undefined;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Re-arms the watchdogs that were on disk when the server stopped.
|
|
96
|
+
*
|
|
97
|
+
* A watchdog whose silence had ALREADY run out while the server was down fires as soon as it comes
|
|
98
|
+
* back: the parent asked to be told after N minutes of silence, and the silence did happen. The
|
|
99
|
+
* remaining time is measured from the last sign of life, not from the restart.
|
|
100
|
+
*/
|
|
101
|
+
async restore() {
|
|
102
|
+
for (const workspaceId of this.host.workspaceIds()) {
|
|
103
|
+
const path = this.statePath(workspaceId);
|
|
104
|
+
if (!path)
|
|
105
|
+
continue;
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// A missing file is the normal case (no watchdog was ever declared). A corrupt one is not
|
|
112
|
+
// fatal either: the worst case of losing it is that the parent stops being reminded, which
|
|
113
|
+
// is the state it was in before this feature existed.
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const records = parsed?.watchdogs;
|
|
117
|
+
if (!records || typeof records !== "object")
|
|
118
|
+
continue;
|
|
119
|
+
for (const [childConversationId, record] of Object.entries(records)) {
|
|
120
|
+
if (!record || typeof record.timeoutSeconds !== "number" || !record.parentConversationId)
|
|
121
|
+
continue;
|
|
122
|
+
const entry = {
|
|
123
|
+
childConversationId,
|
|
124
|
+
parentConversationId: record.parentConversationId,
|
|
125
|
+
workspaceId: record.workspaceId ?? workspaceId,
|
|
126
|
+
timeoutSeconds: record.timeoutSeconds,
|
|
127
|
+
lastSignalAt: typeof record.lastSignalAt === "number" ? record.lastSignalAt : Date.now(),
|
|
128
|
+
endedReminded: Boolean(record.endedReminded),
|
|
129
|
+
};
|
|
130
|
+
this.entries.set(childConversationId, entry);
|
|
131
|
+
this.schedule(entry);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Stops every timer. The process is going away; the state stays on disk. */
|
|
136
|
+
dispose() {
|
|
137
|
+
for (const timer of this.timers.values())
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
this.timers.clear();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Arms the timer for one entry.
|
|
143
|
+
*
|
|
144
|
+
* THE TIMER IS UNREF'D. A pending reminder must not keep the Node process alive: the server is
|
|
145
|
+
* allowed to shut down with a watchdog in flight, and the state is on disk anyway.
|
|
146
|
+
*/
|
|
147
|
+
schedule(entry) {
|
|
148
|
+
this.clearTimer(entry.childConversationId);
|
|
149
|
+
const elapsed = Date.now() - entry.lastSignalAt;
|
|
150
|
+
const remaining = Math.max(0, entry.timeoutSeconds * 1000 - elapsed);
|
|
151
|
+
const timer = setTimeout(() => {
|
|
152
|
+
void this.fire(entry.childConversationId);
|
|
153
|
+
}, remaining);
|
|
154
|
+
timer.unref?.();
|
|
155
|
+
this.timers.set(entry.childConversationId, timer);
|
|
156
|
+
}
|
|
157
|
+
clearTimer(childConversationId) {
|
|
158
|
+
const timer = this.timers.get(childConversationId);
|
|
159
|
+
if (timer)
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
this.timers.delete(childConversationId);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The clock ran out.
|
|
165
|
+
*
|
|
166
|
+
* THE CHILD'S STATE DECIDES WHETHER THIS IS A REPEAT, AND THE REPEAT IS NOT LOUDER - IT IS THE
|
|
167
|
+
* SAME. Every interval the parent gets the same reminder, because the parent's need is the same:
|
|
168
|
+
* "I still have no news". An interval that grew would make the watchdog quietest exactly when the
|
|
169
|
+
* child has been silent longest, which is backwards.
|
|
170
|
+
*
|
|
171
|
+
* THE ONE EXCEPTION IS A CHILD THAT IS NO LONGER WORKING. A child whose turn ended without reporting
|
|
172
|
+
* is not going to report: the turn is over. The parent is told once that the child has gone quiet
|
|
173
|
+
* AND stopped, and the watchdog then takes itself off - a reminder that repeats for a child that
|
|
174
|
+
* cannot speak any more is noise, not information.
|
|
175
|
+
*/
|
|
176
|
+
async fire(childConversationId) {
|
|
177
|
+
const entry = this.entries.get(childConversationId);
|
|
178
|
+
if (!entry)
|
|
179
|
+
return;
|
|
180
|
+
const childWorking = this.host.isChildWorking(childConversationId);
|
|
181
|
+
if (!childWorking && entry.endedReminded) {
|
|
182
|
+
await this.cancel(childConversationId);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const elapsedSeconds = Math.max(0, Math.round((Date.now() - entry.lastSignalAt) / 1000));
|
|
186
|
+
try {
|
|
187
|
+
await this.host.onTimeout({
|
|
188
|
+
childConversationId,
|
|
189
|
+
parentConversationId: entry.parentConversationId,
|
|
190
|
+
timeoutSeconds: entry.timeoutSeconds,
|
|
191
|
+
elapsedSeconds,
|
|
192
|
+
childWorking,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
this.host.warn?.(`Super Pi: watchdog reminder for ${childConversationId} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
197
|
+
}
|
|
198
|
+
entry.endedReminded = !childWorking;
|
|
199
|
+
entry.lastSignalAt = Date.now();
|
|
200
|
+
await this.persist(entry.workspaceId);
|
|
201
|
+
this.schedule(entry);
|
|
202
|
+
}
|
|
203
|
+
/** The path of one workspace's state file, or undefined when the workspace is unknown. */
|
|
204
|
+
statePath(workspaceId) {
|
|
205
|
+
const workspacePath = this.host.workspacePath(workspaceId);
|
|
206
|
+
if (!workspacePath)
|
|
207
|
+
return undefined;
|
|
208
|
+
return join(workspacePath, ".super-pi", WATCHDOG_STATE_FILE);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Writes one workspace's state file.
|
|
212
|
+
*
|
|
213
|
+
* THE WRITES ARE SERIALIZED PER WORKSPACE. Two watchdogs in one workspace each rewrite the same
|
|
214
|
+
* file, and an interleaved pair of writes would leave a file that describes one watchdog twice and
|
|
215
|
+
* loses the other.
|
|
216
|
+
*/
|
|
217
|
+
async persist(workspaceId) {
|
|
218
|
+
const path = this.statePath(workspaceId);
|
|
219
|
+
if (!path)
|
|
220
|
+
return;
|
|
221
|
+
const previous = this.writes.get(workspaceId) ?? Promise.resolve();
|
|
222
|
+
const next = previous.then(() => this.writeStateFile(path, workspaceId)).catch((error) => {
|
|
223
|
+
this.host.warn?.(`Super Pi: watchdog state could not be written: ${error instanceof Error ? error.message : String(error)}`);
|
|
224
|
+
});
|
|
225
|
+
this.writes.set(workspaceId, next);
|
|
226
|
+
await next;
|
|
227
|
+
}
|
|
228
|
+
async writeStateFile(path, workspaceId) {
|
|
229
|
+
const watchdogs = {};
|
|
230
|
+
for (const entry of this.entries.values()) {
|
|
231
|
+
if (entry.workspaceId !== workspaceId)
|
|
232
|
+
continue;
|
|
233
|
+
watchdogs[entry.childConversationId] = {
|
|
234
|
+
parentConversationId: entry.parentConversationId,
|
|
235
|
+
workspaceId: entry.workspaceId,
|
|
236
|
+
timeoutSeconds: entry.timeoutSeconds,
|
|
237
|
+
lastSignalAt: entry.lastSignalAt,
|
|
238
|
+
endedReminded: entry.endedReminded,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
242
|
+
await writeFile(path, `${JSON.stringify({ version: 1, watchdogs }, null, 2)}\n`, "utf8");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -169,7 +169,7 @@ export class CommandHandler {
|
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
171
171
|
if (command.type === "conversation.create") {
|
|
172
|
-
return await this.conversationService.createConversation(command.workspaceId);
|
|
172
|
+
return await this.conversationService.createConversation(command.workspaceId, undefined, command.cwd);
|
|
173
173
|
}
|
|
174
174
|
if (command.type === "conversation.autoRename") {
|
|
175
175
|
// Answered on a pushed event rather than through the return value: this spends real model time, and
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ConversationContextUsage, SubagentUsage } from "./types.js";
|
|
2
|
+
export type TurnUsageReadout = {
|
|
3
|
+
usage?: SubagentUsage;
|
|
4
|
+
costKnown: boolean;
|
|
5
|
+
contextUsage?: ConversationContextUsage;
|
|
6
|
+
};
|
|
7
|
+
/** Collect only completed assistant messages, never the repeated usage on streaming deltas. */
|
|
8
|
+
export declare class ConversationUsageCollector {
|
|
9
|
+
private readonly total;
|
|
10
|
+
private costKnown;
|
|
11
|
+
add(value: unknown): void;
|
|
12
|
+
readout(context: {
|
|
13
|
+
tokens: number | null;
|
|
14
|
+
contextWindow: number;
|
|
15
|
+
} | undefined, model: {
|
|
16
|
+
provider?: string;
|
|
17
|
+
id?: string;
|
|
18
|
+
} | undefined, agentDir: string): TurnUsageReadout;
|
|
19
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { effectiveContextWindow, readDeclaredWindows } from "./model-context-window.js";
|
|
2
|
+
const nonnegative = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
3
|
+
/** Collect only completed assistant messages, never the repeated usage on streaming deltas. */
|
|
4
|
+
export class ConversationUsageCollector {
|
|
5
|
+
total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
6
|
+
costKnown = true;
|
|
7
|
+
add(value) {
|
|
8
|
+
if (!value || typeof value !== "object") {
|
|
9
|
+
this.costKnown = false;
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const usage = value;
|
|
13
|
+
const keys = ["input", "output", "cacheRead", "cacheWrite"];
|
|
14
|
+
const cost = usage.cost?.total;
|
|
15
|
+
if (!nonnegative(cost))
|
|
16
|
+
this.costKnown = false;
|
|
17
|
+
if (!keys.some(key => nonnegative(usage[key])) && !nonnegative(cost))
|
|
18
|
+
return;
|
|
19
|
+
for (const key of keys)
|
|
20
|
+
if (nonnegative(usage[key]))
|
|
21
|
+
this.total[key] += usage[key];
|
|
22
|
+
if (nonnegative(cost))
|
|
23
|
+
this.total.cost += cost;
|
|
24
|
+
this.total.turns++;
|
|
25
|
+
}
|
|
26
|
+
readout(context, model, agentDir) {
|
|
27
|
+
let contextUsage;
|
|
28
|
+
if (context) {
|
|
29
|
+
const contextWindow = model?.provider && model.id
|
|
30
|
+
? effectiveContextWindow(readDeclaredWindows(agentDir), model.provider, model.id, context.contextWindow) ?? context.contextWindow
|
|
31
|
+
: context.contextWindow;
|
|
32
|
+
if (nonnegative(contextWindow) && contextWindow > 0) {
|
|
33
|
+
// Pi returns null after compaction until another assistant supplies a trustworthy token count.
|
|
34
|
+
const tokens = nonnegative(context.tokens) ? context.tokens : null;
|
|
35
|
+
contextUsage = { tokens, contextWindow, percent: tokens === null ? null : tokens / contextWindow * 100 };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { ...(this.total.turns ? { usage: { ...this.total } } : {}), costKnown: this.costKnown, ...(contextUsage ? { contextUsage } : {}) };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -35,4 +35,7 @@ export declare function computeFileDiff(options: {
|
|
|
35
35
|
preferred?: "auto" | DiffBaselineKind;
|
|
36
36
|
tracker?: FileChangeTracker;
|
|
37
37
|
workspaceId?: string;
|
|
38
|
+
/** Repository root and tracker key when `root` is a session subdirectory. */
|
|
39
|
+
gitRoot?: string;
|
|
40
|
+
trackerPath?: string;
|
|
38
41
|
}): Promise<FileDiff>;
|
package/dist/server/file-diff.js
CHANGED
|
@@ -36,12 +36,12 @@ import { FilesError, resolveInsideRoot } from "./files.js";
|
|
|
36
36
|
* they are looking at and it must not silently become a different one.
|
|
37
37
|
*/
|
|
38
38
|
export async function computeFileDiff(options) {
|
|
39
|
-
const { root, path, preferred = "auto", tracker, workspaceId } = options;
|
|
39
|
+
const { root, path, preferred = "auto", tracker, workspaceId, gitRoot = root, trackerPath = path } = options;
|
|
40
40
|
const after = await readAfter(root, path);
|
|
41
41
|
// The agent's baseline. It is the RIGHT one whenever it exists, because it is the content the agent
|
|
42
42
|
// actually started from - which is not necessarily HEAD, and not necessarily what the file looked
|
|
43
43
|
// like when the server started.
|
|
44
|
-
const agentChange = tracker && workspaceId ? tracker.get(workspaceId,
|
|
44
|
+
const agentChange = tracker && workspaceId ? tracker.get(workspaceId, trackerPath) : undefined;
|
|
45
45
|
if ((preferred === "auto" || preferred === "agent") && agentChange) {
|
|
46
46
|
return {
|
|
47
47
|
path,
|
|
@@ -55,8 +55,8 @@ export async function computeFileDiff(options) {
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
if (preferred === "auto" || preferred === "git") {
|
|
58
|
-
if (await isGitRepository(
|
|
59
|
-
const head = await gitHeadContent(
|
|
58
|
+
if (await isGitRepository(gitRoot)) {
|
|
59
|
+
const head = await gitHeadContent(gitRoot, trackerPath);
|
|
60
60
|
if (head !== null) {
|
|
61
61
|
return { path, baseline: "git", before: head, after, label: "Git 已提交的版本" };
|
|
62
62
|
}
|
|
@@ -10,6 +10,7 @@ export type IndexedFile = {
|
|
|
10
10
|
export type FileIndexEvent = {
|
|
11
11
|
type: "files.index";
|
|
12
12
|
workspaceId: string;
|
|
13
|
+
cwd?: string;
|
|
13
14
|
version: number;
|
|
14
15
|
changed: IndexedFile[];
|
|
15
16
|
removed: string[];
|
|
@@ -33,6 +34,7 @@ export type FileIndexSnapshot = {
|
|
|
33
34
|
export declare class FileIndex {
|
|
34
35
|
readonly workspaceId: string;
|
|
35
36
|
private readonly broadcast;
|
|
37
|
+
readonly cwd: string;
|
|
36
38
|
readonly entries: Map<string, IndexedFile>;
|
|
37
39
|
private readonly history;
|
|
38
40
|
private root;
|
|
@@ -42,7 +44,7 @@ export declare class FileIndex {
|
|
|
42
44
|
version: number;
|
|
43
45
|
builtAt: number;
|
|
44
46
|
complete: boolean;
|
|
45
|
-
constructor(workspaceId: string, broadcast: (event: FileIndexEvent) => void);
|
|
47
|
+
constructor(workspaceId: string, broadcast: (event: FileIndexEvent) => void, cwd?: string);
|
|
46
48
|
start(root: string): void;
|
|
47
49
|
stop(): void;
|
|
48
50
|
snapshot(since?: number): FileIndexSnapshot;
|
|
@@ -83,4 +85,4 @@ export declare class FileIndex {
|
|
|
83
85
|
* said out loud, and the turn carries on. The index is a convenience; it must never be able to end a
|
|
84
86
|
* conversation.
|
|
85
87
|
*/
|
|
86
|
-
export declare function refreshWorkspaceIndex(indexes: Map<string, FileIndex> | undefined, workspaceId: string, path: string): void;
|
|
88
|
+
export declare function refreshWorkspaceIndex(indexes: Map<string, FileIndex> | undefined, workspaceId: string, path: string, workspaceRoot?: string): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
2
3
|
import { SEARCH_SKIP_DIRS, createRootGuard, fileMeta, isTextName, toRelative, toWorkspaceRelative } from "./files.js";
|
|
3
4
|
const MAX_DIRECTORIES = 4000;
|
|
4
5
|
const MAX_FILES = 30000;
|
|
@@ -12,6 +13,7 @@ const MAX_MS = 4000;
|
|
|
12
13
|
export class FileIndex {
|
|
13
14
|
workspaceId;
|
|
14
15
|
broadcast;
|
|
16
|
+
cwd;
|
|
15
17
|
entries = new Map();
|
|
16
18
|
history = [];
|
|
17
19
|
root = "";
|
|
@@ -21,9 +23,10 @@ export class FileIndex {
|
|
|
21
23
|
version = 0;
|
|
22
24
|
builtAt = 0;
|
|
23
25
|
complete = false;
|
|
24
|
-
constructor(workspaceId, broadcast) {
|
|
26
|
+
constructor(workspaceId, broadcast, cwd = "") {
|
|
25
27
|
this.workspaceId = workspaceId;
|
|
26
28
|
this.broadcast = broadcast;
|
|
29
|
+
this.cwd = cwd;
|
|
27
30
|
}
|
|
28
31
|
start(root) {
|
|
29
32
|
if (this.building || this.builtAt)
|
|
@@ -162,7 +165,7 @@ export class FileIndex {
|
|
|
162
165
|
if (!changed.length && !removed.length && !force)
|
|
163
166
|
return;
|
|
164
167
|
this.version += 1;
|
|
165
|
-
const event = { type: "files.index", workspaceId: this.workspaceId, version: this.version, changed, removed, complete: this.complete, skipped: this.skipped };
|
|
168
|
+
const event = { type: "files.index", workspaceId: this.workspaceId, ...(this.cwd ? { cwd: this.cwd } : {}), version: this.version, changed, removed, complete: this.complete, skipped: this.skipped };
|
|
166
169
|
this.history.push({ version: this.version, changed, removed });
|
|
167
170
|
if (this.history.length > 128)
|
|
168
171
|
this.history.shift();
|
|
@@ -185,11 +188,13 @@ export class FileIndex {
|
|
|
185
188
|
* said out loud, and the turn carries on. The index is a convenience; it must never be able to end a
|
|
186
189
|
* conversation.
|
|
187
190
|
*/
|
|
188
|
-
export function refreshWorkspaceIndex(indexes, workspaceId, path) {
|
|
189
|
-
const index
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
191
|
+
export function refreshWorkspaceIndex(indexes, workspaceId, path, workspaceRoot) {
|
|
192
|
+
for (const index of indexes?.values() ?? []) {
|
|
193
|
+
if (index.workspaceId !== workspaceId)
|
|
194
|
+
continue;
|
|
195
|
+
const target = workspaceRoot && index.cwd ? resolve(workspaceRoot, path) : path;
|
|
196
|
+
void index.refresh(target).catch((error) => {
|
|
197
|
+
console.warn(`Super Pi: the file index did not refresh ${path}: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
195
200
|
}
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* the agent's writes - go through this one function, so there is exactly one index per workspace and
|
|
6
6
|
* the two never drift into answering from different maps.
|
|
7
7
|
*/
|
|
8
|
-
export declare function ensureNameIndex(indexes: Map<string, FileNameIndex>, workspaceId: string, root: string): FileNameIndex;
|
|
8
|
+
export declare function ensureNameIndex(indexes: Map<string, FileNameIndex>, workspaceId: string, root: string, cwd?: string): FileNameIndex;
|
|
9
|
+
/** Keeps every root/subdirectory index for a workspace in step with one observed write. */
|
|
10
|
+
export declare function recordWorkspacePath(indexes: Map<string, FileNameIndex>, workspaceId: string, workspaceRoot: string, path: string): void;
|
|
9
11
|
/** What the index knows about one path. Deliberately small: no size, no etag, no mtime. */
|
|
10
12
|
export type NameEntry = {
|
|
11
13
|
/** Workspace-relative, `/`-separated. */
|
|
@@ -60,6 +62,7 @@ export declare function compareNameHits(a: NameEntry, b: NameEntry, needle: stri
|
|
|
60
62
|
*/
|
|
61
63
|
export declare class FileNameIndex {
|
|
62
64
|
readonly workspaceId: string;
|
|
65
|
+
readonly cwd: string;
|
|
63
66
|
readonly entries: Map<string, NameEntry>;
|
|
64
67
|
private root;
|
|
65
68
|
private building;
|
|
@@ -67,7 +70,7 @@ export declare class FileNameIndex {
|
|
|
67
70
|
private scanned;
|
|
68
71
|
private stopped;
|
|
69
72
|
private incomplete;
|
|
70
|
-
constructor(workspaceId: string);
|
|
73
|
+
constructor(workspaceId: string, cwd?: string);
|
|
71
74
|
/**
|
|
72
75
|
* Whether the first pass has FINISHED.
|
|
73
76
|
*
|