ompchamber 0.6.0 → 0.7.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/README.md +24 -8
- package/dist/client/index.html +1 -1
- package/dist/client/static/js/async/{g.8515cbc6f7.js → g.86e2256a70.js} +2 -2
- package/dist/client/static/js/{index.47d3b04d5f.js → index.eccad0b70a.js} +3 -3
- package/package.json +3 -1
- package/src/cli/lib/args.js +1 -1
- package/src/cli/lib/commands/restart.js +11 -0
- package/src/cli/lib/commands/update.js +21 -7
- package/src/cli/lib/runtime.js +6 -3
- package/src/client/hooks/chat/omp/index.ts +4 -109
- package/src/client/hooks/chat/omp/prompt-send.ts +161 -0
- package/src/client/hooks/chat/timeline/send.ts +3 -2
- package/src/server/index.ts +4 -3
- package/src/server/lib/lifecycle/bun.ts +18 -0
- package/src/server/lib/lifecycle/launch-mode.test.ts +32 -0
- package/src/server/lib/lifecycle/launch-mode.ts +40 -0
- package/src/server/lib/lifecycle/restart.test.ts +21 -0
- package/src/server/lib/lifecycle/restart.ts +111 -0
- package/src/server/lib/omp/rpc/session-commands.ts +15 -1
- package/src/server/lib/updates/apply.ts +8 -5
- package/src/server/lib/updates/install.ts +7 -4
- package/src/server/routes/health/probe.ts +2 -1
- package/src/shared/lib/omp/session/stream-state.server.ts +16 -1
- /package/dist/client/static/js/async/{g.8515cbc6f7.js.LICENSE.txt → g.86e2256a70.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{index.47d3b04d5f.js.LICENSE.txt → index.eccad0b70a.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Replacing the instance that is serving an update request.
|
|
8
|
+
*
|
|
9
|
+
* A server cannot restart itself: the process holding the port has to exit
|
|
10
|
+
* before a replacement can bind it, and the update response has to leave first.
|
|
11
|
+
* So once the files on disk are replaced, the work goes to a detached
|
|
12
|
+
* `ompchamber restart --port <port>` — the same stop-then-serve the CLI runs
|
|
13
|
+
* after `ompchamber update`.
|
|
14
|
+
*
|
|
15
|
+
* Instances the CLI did not start are left alone. `bun run dev` and
|
|
16
|
+
* `bun run start` execute this package's source in the user's own shell, and an
|
|
17
|
+
* external supervisor (systemd, pm2, a container) owns the process it started;
|
|
18
|
+
* all of them record `direct`, and replacing any of them would break a loop
|
|
19
|
+
* OMPChamber does not own — or fight a second daemon for the port. Their own
|
|
20
|
+
* launcher is what brings the new files up.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync } from 'node:fs';
|
|
24
|
+
import { join as joinPath, resolve as resolvePath } from 'node:path';
|
|
25
|
+
|
|
26
|
+
import { resolveBunBin } from '@/server/lib/lifecycle/bun';
|
|
27
|
+
import { listInstanceRecords } from '@/server/lib/lifecycle/instance';
|
|
28
|
+
import { resolveLaunchMode } from '@/server/lib/lifecycle/launch-mode';
|
|
29
|
+
|
|
30
|
+
/** Package root of the running copy: src/server/lib/lifecycle -> four levels up. */
|
|
31
|
+
const PKG_ROOT = resolvePath(import.meta.dir, '..', '..', '..', '..');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Time between answering the update request and stopping the server. The
|
|
35
|
+
* response is a small JSON body on a local listener; this is the margin that
|
|
36
|
+
* keeps the console's toast from dying with the process that sent it.
|
|
37
|
+
*/
|
|
38
|
+
export const RESTART_GRACE_MS = 1_200;
|
|
39
|
+
|
|
40
|
+
export interface SelfRestartPlan {
|
|
41
|
+
/** A detached restart is armed; `message` says so. */
|
|
42
|
+
restarting: boolean;
|
|
43
|
+
/** What to tell the user, either way. */
|
|
44
|
+
message: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* True when the CLI owns this instance's lifecycle and may replace it. `direct`
|
|
49
|
+
* is every server OMPChamber did not start: `bun run dev`, `bun run start`, a
|
|
50
|
+
* manual `bun src/server/index.ts`, or an external supervisor.
|
|
51
|
+
*/
|
|
52
|
+
export function isAutoRestartable(launchMode: string | null | undefined): boolean {
|
|
53
|
+
return launchMode === 'daemon' || launchMode === 'foreground';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One-line reason an instance is left running, shared by the CLI and console. */
|
|
57
|
+
export function skipRestartNote(launchMode: string | null | undefined): string {
|
|
58
|
+
if (launchMode === 'direct') {
|
|
59
|
+
return 'this instance runs from source (`bun run dev` / `bun run start`) — start it again to apply the update';
|
|
60
|
+
}
|
|
61
|
+
return 'this instance is not managed by the ompchamber CLI — restart it yourself to apply the update';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The record this process wrote for itself; falls back to the env it was launched with. */
|
|
65
|
+
function ownInstance(): { port: number; launchMode: string } | null {
|
|
66
|
+
const record = listInstanceRecords().find((entry) => entry.pid === process.pid);
|
|
67
|
+
if (record) return { port: record.port, launchMode: record.launchMode };
|
|
68
|
+
|
|
69
|
+
const port = Number(Bun.env.PORT);
|
|
70
|
+
if (!Number.isFinite(port) || port <= 0) return null;
|
|
71
|
+
return { port, launchMode: resolveLaunchMode() };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** `ompchamber restart --port <port>` for the install this server runs from. */
|
|
75
|
+
function restartCommand(port: number): string[] | null {
|
|
76
|
+
const cli = joinPath(PKG_ROOT, 'src', 'cli', 'ompchamber.js');
|
|
77
|
+
if (!existsSync(cli)) return null;
|
|
78
|
+
return [resolveBunBin(), cli, 'restart', '--port', String(port)];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Stop and start this port again, once the response has been delivered. The
|
|
83
|
+
* helper is detached so this process exiting does not take it down, and it
|
|
84
|
+
* removes this port's record itself (`stopInstance`) before serving.
|
|
85
|
+
*/
|
|
86
|
+
export function scheduleSelfRestart(): SelfRestartPlan {
|
|
87
|
+
const self = ownInstance();
|
|
88
|
+
if (!self) {
|
|
89
|
+
return { restarting: false, message: 'This instance could not be identified — restart it yourself to apply the update.' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!isAutoRestartable(self.launchMode)) {
|
|
93
|
+
return { restarting: false, message: skipRestartNote(self.launchMode) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const cmd = restartCommand(self.port);
|
|
97
|
+
if (!cmd) {
|
|
98
|
+
return { restarting: false, message: `The ompchamber CLI is missing from ${PKG_ROOT} — restart it yourself to apply the update.` };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
console.log(`[ompchamber] restarting on port ${self.port}: ${cmd.join(' ')}`);
|
|
103
|
+
try {
|
|
104
|
+
Bun.spawn({ cmd, cwd: PKG_ROOT, stdin: 'ignore', stdout: 'inherit', stderr: 'inherit', detached: true }).unref();
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error(`[ompchamber] restart helper could not be started: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
}
|
|
108
|
+
}, RESTART_GRACE_MS);
|
|
109
|
+
|
|
110
|
+
return { restarting: true, message: `Restarting OMPChamber on port ${self.port} to apply it — the console reconnects on its own.` };
|
|
111
|
+
}
|
|
@@ -13,7 +13,7 @@ import { RpcCommandTimeoutError, type RpcProcess } from '@/server/lib/omp/rpc/pr
|
|
|
13
13
|
import { AWAITING_AGENT_START_TIMEOUT_MS, GET_STATE_TIMEOUT_MS, IMAGE_BEARING_COMMANDS, PASSTHROUGH_COMMANDS, PROMPT_ACK_TIMEOUT_MS, RESTARTING_MESSAGE, SESSION_BUSY_MESSAGE, WebRpcError, toImageContents, type AgentEvent, type RpcSessionState, validateAgentImages } from '@/server/lib/omp/rpc/constants';
|
|
14
14
|
import { clearSessionFileCaches } from '@/server/lib/omp/session/files';
|
|
15
15
|
import { notifyRunningChange } from '@/server/lib/omp/rpc/session-registry';
|
|
16
|
-
import { markStreamStatus } from '@/shared/lib/omp/session/stream-state.server';
|
|
16
|
+
import { clearStreamStatus, markStreamStatus } from '@/shared/lib/omp/session/stream-state.server';
|
|
17
17
|
import { buildWebState, type WebStateHost } from '@/server/lib/omp/rpc/web-state';
|
|
18
18
|
|
|
19
19
|
/** Runtime surface AgentSessionWrapper exposes to the command dispatcher. */
|
|
@@ -66,12 +66,24 @@ export async function dispatchSessionCommand(host: SessionCommandHost, command:
|
|
|
66
66
|
throw new Error('Cannot send a prompt while a shell command is running');
|
|
67
67
|
}
|
|
68
68
|
const streamingBehavior = command.streamingBehavior as 'steer' | 'followUp' | undefined;
|
|
69
|
+
// The live `stream` row starts with the DISPATCH, not with agent_start:
|
|
70
|
+
// the spawn/ack round trip that precedes agent_start must not read as
|
|
71
|
+
// "nothing happened" in the sidebar after the user hit send. Skipped
|
|
72
|
+
// while a turn already streams — that row belongs to the running turn,
|
|
73
|
+
// and this dispatch's failure must never roll it back.
|
|
74
|
+
const ownsStreamRow = !streamingBehavior && !host.streaming && Boolean(host.sessionId);
|
|
75
|
+
// A prompt that never started a turn leaves no run behind, so the row we
|
|
76
|
+
// wrote must go — unless a turn began meanwhile, which owns it now.
|
|
77
|
+
const releaseStreamRow = (): void => {
|
|
78
|
+
if (ownsStreamRow && !host.streaming) void clearStreamStatus(host.sessionId);
|
|
79
|
+
};
|
|
69
80
|
if (!streamingBehavior) {
|
|
70
81
|
host.promptRunning = true;
|
|
71
82
|
host.promptDispatchPendingCount += 1;
|
|
72
83
|
host.awaitingAgentStart = false;
|
|
73
84
|
host.awaitingAgentStartDeadline = 0;
|
|
74
85
|
host.continuationGraceUntil = 0;
|
|
86
|
+
if (ownsStreamRow) void markStreamStatus(host.sessionId, 'stream');
|
|
75
87
|
notifyRunningChange();
|
|
76
88
|
}
|
|
77
89
|
try {
|
|
@@ -85,6 +97,7 @@ export async function dispatchSessionCommand(host: SessionCommandHost, command:
|
|
|
85
97
|
host.promptRunning = false;
|
|
86
98
|
host.awaitingAgentStart = false;
|
|
87
99
|
host.awaitingAgentStartDeadline = 0;
|
|
100
|
+
releaseStreamRow();
|
|
88
101
|
host.emit({ type: 'prompt_result', agentInvoked: false });
|
|
89
102
|
notifyRunningChange();
|
|
90
103
|
} else if (!streamingBehavior && ack?.agentInvoked !== false) {
|
|
@@ -95,6 +108,7 @@ export async function dispatchSessionCommand(host: SessionCommandHost, command:
|
|
|
95
108
|
host.promptRunning = false;
|
|
96
109
|
host.awaitingAgentStart = false;
|
|
97
110
|
host.awaitingAgentStartDeadline = 0;
|
|
111
|
+
releaseStreamRow();
|
|
98
112
|
notifyRunningChange();
|
|
99
113
|
// The ack may simply be queued behind a running turn: omp accepts the
|
|
100
114
|
// prompt before the turn it starts. No reset, and the client must not
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Applies a requested update. oh-my-pi is updated through the omp binary;
|
|
3
3
|
* OMPChamber through the shared self-update engine, which replaces the running
|
|
4
|
-
* install in place (bun global install, or git fast-forward + rebuild)
|
|
4
|
+
* install in place (bun global install, or git fast-forward + rebuild) and then
|
|
5
|
+
* restarts this instance through a detached `ompchamber restart` — unless the
|
|
6
|
+
* instance was started from source, which its own launcher has to restart.
|
|
5
7
|
*/
|
|
6
8
|
|
|
9
|
+
import { scheduleSelfRestart } from '@/server/lib/lifecycle/restart';
|
|
7
10
|
import { updateOmpChamber } from '@/server/lib/updates/install';
|
|
8
11
|
import { applyOmpUpdate } from '@/server/lib/updates/omp';
|
|
9
12
|
import type { UpdateApplyResult, UpdateTarget } from '@/shared/types/updates';
|
|
10
13
|
|
|
11
|
-
/** A server cannot replace its own process, so the user restarts it. */
|
|
12
|
-
const RESTART_NOTE = ' Restart the server (`ompchamber restart`) to apply it.';
|
|
13
|
-
|
|
14
14
|
export async function applyUpdate(target: UpdateTarget): Promise<UpdateApplyResult> {
|
|
15
15
|
if (target === 'omp') {
|
|
16
16
|
try {
|
|
@@ -28,11 +28,14 @@ export async function applyUpdate(target: UpdateTarget): Promise<UpdateApplyResu
|
|
|
28
28
|
|
|
29
29
|
if (target === 'ompchamber') {
|
|
30
30
|
const result = await updateOmpChamber();
|
|
31
|
+
// Only a call that actually replaced files has something to restart for:
|
|
32
|
+
// an "already up to date" answer must not bounce a healthy server.
|
|
33
|
+
const restart = result.success && result.updated ? scheduleSelfRestart() : null;
|
|
31
34
|
return {
|
|
32
35
|
success: result.success,
|
|
33
36
|
target: 'ompchamber',
|
|
34
37
|
manual: result.manual,
|
|
35
|
-
message: `${result.message}${
|
|
38
|
+
message: restart ? `${result.message} ${restart.message}` : result.message,
|
|
36
39
|
output: result.output || undefined,
|
|
37
40
|
};
|
|
38
41
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { join as joinPath, resolve as resolvePath } from 'node:path';
|
|
16
16
|
|
|
17
|
+
import { resolveBunBin } from '@/server/lib/lifecycle/bun';
|
|
17
18
|
import { fetchLatestRelease, type GitHubRelease } from '@/server/lib/updates/github';
|
|
18
19
|
import {
|
|
19
20
|
detectInstallMethod,
|
|
@@ -137,10 +138,12 @@ export function resolveInstallContext(pkgRoot = DEFAULT_PKG_ROOT): InstallContex
|
|
|
137
138
|
const remote = gitWorkTree ? runSync(['git', '-C', root, 'remote', 'get-url', 'origin']) : null;
|
|
138
139
|
const gitRemote = remote?.ok && remote.out.length > 0 ? remote.out : null;
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
return {
|
|
142
|
+
...detectInstallMethod({ pkgRoot: root, gitWorkTree, gitRemote }),
|
|
143
|
+
pkgRoot: root,
|
|
144
|
+
gitRemote,
|
|
145
|
+
bunBin: resolveBunBin(),
|
|
146
|
+
};
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
/** The commands that install `version` for a given install method. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { json, NO_STORE_HEADERS } from '@/server/lib/remix-compat';
|
|
2
2
|
import pkg from '@/../package.json';
|
|
3
|
+
import { resolveLaunchMode } from '@/server/lib/lifecycle/launch-mode';
|
|
3
4
|
import { isMockMode } from '@/server/mock.server';
|
|
4
5
|
|
|
5
6
|
const STARTED_AT = Date.now();
|
|
@@ -26,7 +27,7 @@ export async function loader() {
|
|
|
26
27
|
// `launchMode` are what `status` prints, and the pair with `pid` is what
|
|
27
28
|
// separates a dev server from a stale published build.
|
|
28
29
|
mode: Bun.env.NODE_ENV === 'production' ? 'prod' : 'dev',
|
|
29
|
-
launchMode:
|
|
30
|
+
launchMode: resolveLaunchMode(),
|
|
30
31
|
port: Number(Bun.env.PORT) || 3000,
|
|
31
32
|
host: Bun.env.HOST || 'localhost',
|
|
32
33
|
},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* sidebar can render spinner/check from the SAME loader that already refresh
|
|
9
9
|
* on stream events (`omp:session-updated` → revalidator). Statuses:
|
|
10
10
|
*
|
|
11
|
-
* stream — a run is in flight (agent_start)
|
|
11
|
+
* stream — a run is in flight (prompt dispatch → agent_start)
|
|
12
12
|
* finish — the run ended normally (agent_end), not yet seen
|
|
13
13
|
* abort — the user stopped it, not yet seen
|
|
14
14
|
* error — the run failed (prompt_error / process exit), not yet seen
|
|
@@ -41,6 +41,21 @@ export async function markStreamStatus(sessionId: string, status: SessionStreamS
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Drop an optimistic `stream` row whose prompt never started a turn: the ack
|
|
46
|
+
* reported `agentInvoked: false`, or the dispatch failed before omp accepted
|
|
47
|
+
* it. Live-only — a `stream` row a concurrent `agent_start` wrote is still the
|
|
48
|
+
* truth, and terminal badges are cleared by `markStreamSeen`.
|
|
49
|
+
*/
|
|
50
|
+
export async function clearStreamStatus(sessionId: string): Promise<void> {
|
|
51
|
+
try {
|
|
52
|
+
const db = await getDb();
|
|
53
|
+
await db.run("DELETE FROM session_stream_state WHERE session_id = ? AND status = 'stream'", [sessionId]);
|
|
54
|
+
} catch {
|
|
55
|
+
// Best-effort.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
44
59
|
/**
|
|
45
60
|
* Opening the session clears its terminal badge (one-shot contract).
|
|
46
61
|
*
|
/package/dist/client/static/js/async/{g.8515cbc6f7.js.LICENSE.txt → g.86e2256a70.js.LICENSE.txt}
RENAMED
|
File without changes
|
/package/dist/client/static/js/{index.47d3b04d5f.js.LICENSE.txt → index.eccad0b70a.js.LICENSE.txt}
RENAMED
|
File without changes
|