gestalt-mobile 0.1.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/LICENSE +661 -0
- package/README.md +133 -0
- package/dist/client/assets/index-0wjiXBGW.js +10 -0
- package/dist/client/assets/index-CfoipqOl.css +1 -0
- package/dist/client/branding/dyne-logotype-black.svg +17 -0
- package/dist/client/branding/dyne-logotype-white.svg +17 -0
- package/dist/client/branding/p_glogo_grey.svg +8 -0
- package/dist/client/branding/p_glogo_white.svg +8 -0
- package/dist/client/branding/t_glogo_grey.svg +9 -0
- package/dist/client/branding/t_glogo_white.svg +9 -0
- package/dist/client/index.html +19 -0
- package/dist/server/server/app.js +148 -0
- package/dist/server/server/cli/list-sessions.js +13 -0
- package/dist/server/server/cli.js +126 -0
- package/dist/server/server/composition.js +237 -0
- package/dist/server/server/config.js +37 -0
- package/dist/server/server/features/catalog/application/ports.js +6 -0
- package/dist/server/server/features/catalog/get-bootstrap/endpoint.js +9 -0
- package/dist/server/server/features/catalog/get-bootstrap/request.js +6 -0
- package/dist/server/server/features/catalog/get-bootstrap/response.js +6 -0
- package/dist/server/server/features/catalog/get-bootstrap/use-case.js +19 -0
- package/dist/server/server/features/git/checkout-branch/endpoint.js +27 -0
- package/dist/server/server/features/git/clone-repository/endpoint.js +19 -0
- package/dist/server/server/features/git/get-summary/endpoint.js +13 -0
- package/dist/server/server/features/git/pull-rebase/endpoint.js +22 -0
- package/dist/server/server/features/git/push-policy.js +14 -0
- package/dist/server/server/features/git/push-upstream/endpoint.js +29 -0
- package/dist/server/server/features/git/refresh/endpoint.js +24 -0
- package/dist/server/server/features/git/refresh-policy.js +8 -0
- package/dist/server/server/features/health/get-health/endpoint.js +8 -0
- package/dist/server/server/features/health/get-health/request.js +6 -0
- package/dist/server/server/features/health/get-health/response.js +6 -0
- package/dist/server/server/features/sessions/application/ports.js +6 -0
- package/dist/server/server/features/sessions/application/resume-command.js +22 -0
- package/dist/server/server/features/sessions/application/start-settings.js +6 -0
- package/dist/server/server/features/sessions/forget-session/endpoint.js +15 -0
- package/dist/server/server/features/sessions/get-history/endpoint.js +20 -0
- package/dist/server/server/features/sessions/get-history/history-mapper.js +82 -0
- package/dist/server/server/features/sessions/get-session/endpoint.js +17 -0
- package/dist/server/server/features/sessions/history/use-case.js +8 -0
- package/dist/server/server/features/sessions/interaction/kind.js +8 -0
- package/dist/server/server/features/sessions/interaction/response-guard.js +8 -0
- package/dist/server/server/features/sessions/interaction/response-validator.js +35 -0
- package/dist/server/server/features/sessions/interrupt-turn/endpoint.js +19 -0
- package/dist/server/server/features/sessions/interrupt-turn/use-case.js +10 -0
- package/dist/server/server/features/sessions/lifecycle/use-case.js +15 -0
- package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +11 -0
- package/dist/server/server/features/sessions/list-sessions/endpoint.js +12 -0
- package/dist/server/server/features/sessions/list-sessions/use-case.js +8 -0
- package/dist/server/server/features/sessions/model/errors.js +13 -0
- package/dist/server/server/features/sessions/model/events.js +6 -0
- package/dist/server/server/features/sessions/model/relay-session.js +122 -0
- package/dist/server/server/features/sessions/model/value-objects.js +22 -0
- package/dist/server/server/features/sessions/promote-recent-thread/endpoint.js +29 -0
- package/dist/server/server/features/sessions/promote-recent-thread/use-case.js +26 -0
- package/dist/server/server/features/sessions/release-session/endpoint.js +25 -0
- package/dist/server/server/features/sessions/release-session/use-case.js +9 -0
- package/dist/server/server/features/sessions/respond-interaction/endpoint.js +24 -0
- package/dist/server/server/features/sessions/restore-session/endpoint.js +27 -0
- package/dist/server/server/features/sessions/restore-session/use-case.js +17 -0
- package/dist/server/server/features/sessions/session-events/endpoint.js +44 -0
- package/dist/server/server/features/sessions/start-session/endpoint.js +37 -0
- package/dist/server/server/features/sessions/start-session/request.js +22 -0
- package/dist/server/server/features/sessions/start-session/use-case.js +29 -0
- package/dist/server/server/features/sessions/start-turn/endpoint.js +20 -0
- package/dist/server/server/features/sessions/stop-session/endpoint.js +25 -0
- package/dist/server/server/features/sessions/stop-session/use-case.js +9 -0
- package/dist/server/server/features/sessions/turn/start-policy.js +8 -0
- package/dist/server/server/index.js +7 -0
- package/dist/server/server/main.js +14 -0
- package/dist/server/server/platform/catalog/codex-profile-catalog.js +42 -0
- package/dist/server/server/platform/catalog/filesystem-workspace-catalog.js +60 -0
- package/dist/server/server/platform/catalog/profile-command.js +19 -0
- package/dist/server/server/platform/catalog/workspace-id.js +9 -0
- package/dist/server/server/platform/codex/approval-policy.js +8 -0
- package/dist/server/server/platform/codex/codex-process-launcher.js +27 -0
- package/dist/server/server/platform/codex/error-code.js +10 -0
- package/dist/server/server/platform/codex/json-rpc-client.js +71 -0
- package/dist/server/server/platform/codex/normalizer.js +81 -0
- package/dist/server/server/platform/codex/protocol-compatibility.js +12 -0
- package/dist/server/server/platform/codex/recent-thread-lister.js +49 -0
- package/dist/server/server/platform/codex/server-request.js +14 -0
- package/dist/server/server/platform/codex/session-runtime.js +142 -0
- package/dist/server/server/platform/codex/thread-id.js +10 -0
- package/dist/server/server/platform/events/replay.js +12 -0
- package/dist/server/server/platform/events/session-event-bus.js +17 -0
- package/dist/server/server/platform/events/slow-client.js +9 -0
- package/dist/server/server/platform/events/websocket-heartbeat.js +20 -0
- package/dist/server/server/platform/git/command.js +13 -0
- package/dist/server/server/platform/git/git-fetch-coordinator.js +40 -0
- package/dist/server/server/platform/git/git-inspector.js +116 -0
- package/dist/server/server/platform/git/git-summary-cache.js +26 -0
- package/dist/server/server/platform/git/git-summary.js +8 -0
- package/dist/server/server/platform/http/idempotency.js +9 -0
- package/dist/server/server/platform/http/problem-handler.js +32 -0
- package/dist/server/server/platform/http/problem.js +15 -0
- package/dist/server/server/platform/observability/safe-log.js +9 -0
- package/dist/server/server/platform/persistence/migrate.js +9 -0
- package/dist/server/server/platform/persistence/retention.js +9 -0
- package/dist/server/server/platform/persistence/sqlite-event-journal.js +50 -0
- package/dist/server/server/platform/persistence/sqlite-idempotency-store.js +22 -0
- package/dist/server/server/platform/persistence/sqlite-pending-interaction-store.js +33 -0
- package/dist/server/server/platform/persistence/sqlite-session-repository.js +43 -0
- package/dist/server/server/platform/persistence/sqlite.js +14 -0
- package/dist/server/server/platform/persistence/state-path.js +16 -0
- package/dist/server/server/platform/runtime/clock.js +9 -0
- package/dist/server/server/platform/runtime/concurrency.js +15 -0
- package/dist/server/server/platform/runtime/retry-policy.js +9 -0
- package/dist/server/server/platform/runtime/session-supervisor.js +41 -0
- package/dist/server/shared/contracts/problem.js +6 -0
- package/dist/server/shared/contracts/session-event.js +6 -0
- package/package.json +78 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export function registerGetGitSummary(app, deps) {
|
|
7
|
+
app.get('/api/sessions/:id/git', async (request, reply) => {
|
|
8
|
+
const session = deps.find(request.params.id);
|
|
9
|
+
return session
|
|
10
|
+
? reply.send(await deps.inspect(session.workspacePath))
|
|
11
|
+
: reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export function registerPullRebase(app, deps) {
|
|
7
|
+
app.post('/api/sessions/:id/git/pull', async (request, reply) => {
|
|
8
|
+
const session = deps.find(request.params.id);
|
|
9
|
+
if (!session)
|
|
10
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
11
|
+
try {
|
|
12
|
+
await deps.pull(session.workspacePath);
|
|
13
|
+
return reply.code(202).send({ accepted: true });
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
return reply.code(409).send({
|
|
17
|
+
code: 'GIT_PULL_FAILED',
|
|
18
|
+
detail: error instanceof Error ? error.message : 'Git pull --rebase failed.',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export function mayPush(input) {
|
|
7
|
+
if (!input.upstream)
|
|
8
|
+
return { allowed: false, reason: 'NO_UPSTREAM' };
|
|
9
|
+
if (input.ahead < 1)
|
|
10
|
+
return { allowed: false, reason: 'NOT_AHEAD' };
|
|
11
|
+
if (input.behind > 0)
|
|
12
|
+
return { allowed: false, reason: 'BEHIND_UPSTREAM' };
|
|
13
|
+
return { allowed: true };
|
|
14
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { idempotencyKey } from '../../../platform/http/idempotency.js';
|
|
7
|
+
import { mayPush } from '../push-policy.js';
|
|
8
|
+
export function registerPushUpstream(app, deps) {
|
|
9
|
+
app.post('/api/sessions/:id/git/push', async (request, reply) => {
|
|
10
|
+
const id = request.params.id;
|
|
11
|
+
const key = idempotencyKey(request.headers);
|
|
12
|
+
const scope = `push-git:${id}`;
|
|
13
|
+
const prior = key ? deps.idempotency?.get(scope, key) : null;
|
|
14
|
+
if (prior)
|
|
15
|
+
return reply.code(prior.statusCode).send(JSON.parse(prior.body));
|
|
16
|
+
const session = deps.find(id);
|
|
17
|
+
if (!session)
|
|
18
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
19
|
+
const summary = await deps.inspect(session.workspacePath);
|
|
20
|
+
const allowed = mayPush(summary);
|
|
21
|
+
if (!allowed.allowed || !summary.upstream)
|
|
22
|
+
return reply.code(409).send({ code: allowed.reason ?? 'GIT_PUSH_UNAVAILABLE' });
|
|
23
|
+
await deps.push(session.workspacePath, summary.upstream);
|
|
24
|
+
const body = { accepted: true };
|
|
25
|
+
if (key)
|
|
26
|
+
deps.idempotency?.put(scope, key, 202, JSON.stringify(body));
|
|
27
|
+
return reply.code(202).send(body);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { idempotencyKey } from '../../../platform/http/idempotency.js';
|
|
7
|
+
export function registerRefreshGit(app, deps) {
|
|
8
|
+
app.post('/api/sessions/:id/git/refresh', async (request, reply) => {
|
|
9
|
+
const id = request.params.id;
|
|
10
|
+
const key = idempotencyKey(request.headers);
|
|
11
|
+
const scope = `refresh-git:${id}`;
|
|
12
|
+
const prior = key ? deps.idempotency?.get(scope, key) : null;
|
|
13
|
+
if (prior)
|
|
14
|
+
return reply.code(prior.statusCode).send(JSON.parse(prior.body));
|
|
15
|
+
const session = deps.find(id);
|
|
16
|
+
if (!session)
|
|
17
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
18
|
+
await deps.refresh(session.workspacePath);
|
|
19
|
+
const body = { accepted: true };
|
|
20
|
+
if (key)
|
|
21
|
+
deps.idempotency?.put(scope, key, 202, JSON.stringify(body));
|
|
22
|
+
return reply.code(202).send(body);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export function mayFetch(lastFetchedAt, now, throttleMs = 60_000) {
|
|
7
|
+
return lastFetchedAt === null || now - lastFetchedAt >= throttleMs;
|
|
8
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
const quote = (value) => `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
7
|
+
export function buildResumeCommand(session) {
|
|
8
|
+
if (!session.threadId)
|
|
9
|
+
throw new Error('THREAD_REQUIRED');
|
|
10
|
+
return [
|
|
11
|
+
'codex-profile',
|
|
12
|
+
'cli',
|
|
13
|
+
session.profile,
|
|
14
|
+
'resume',
|
|
15
|
+
session.threadId,
|
|
16
|
+
'-C',
|
|
17
|
+
session.workspacePath,
|
|
18
|
+
'--include-non-interactive',
|
|
19
|
+
]
|
|
20
|
+
.map(quote)
|
|
21
|
+
.join(' ');
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export function registerForgetSession(app, deps) {
|
|
7
|
+
app.delete('/api/sessions/:id', async (request, reply) => {
|
|
8
|
+
const id = request.params.id;
|
|
9
|
+
if (!deps.find(id))
|
|
10
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
11
|
+
deps.close(id);
|
|
12
|
+
deps.remove(id);
|
|
13
|
+
return reply.code(204).send();
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { toChatItems } from './history-mapper.js';
|
|
7
|
+
export function registerGetHistory(app, deps) {
|
|
8
|
+
app.get('/api/sessions/:id/history', async (request, reply) => {
|
|
9
|
+
const session = deps.find(request.params.id);
|
|
10
|
+
if (!session)
|
|
11
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
12
|
+
const history = await deps.read(session);
|
|
13
|
+
const items = toChatItems(history.turns);
|
|
14
|
+
return reply.send({
|
|
15
|
+
items,
|
|
16
|
+
activeTurnId: history.activeTurnId,
|
|
17
|
+
currentSequence: deps.currentSequence(session.id),
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export function toChatItems(turns) {
|
|
7
|
+
return turns.flatMap((turn) => turn.items.flatMap((item) => toChatItem(item, occurredAt(item, turn))));
|
|
8
|
+
}
|
|
9
|
+
function toChatItem(item, timestamp) {
|
|
10
|
+
return [item].flatMap((item) => {
|
|
11
|
+
const id = typeof item.id === 'string' ? item.id : null;
|
|
12
|
+
if (!id)
|
|
13
|
+
return [];
|
|
14
|
+
switch (item.type) {
|
|
15
|
+
case 'userMessage': {
|
|
16
|
+
const text = Array.isArray(item.content)
|
|
17
|
+
? item.content
|
|
18
|
+
.filter(isRecord)
|
|
19
|
+
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
|
20
|
+
.map((part) => part.text)
|
|
21
|
+
.join('\n')
|
|
22
|
+
: '';
|
|
23
|
+
return text
|
|
24
|
+
? [{ id, kind: 'user', text, ...(timestamp ? { occurredAt: timestamp } : {}) }]
|
|
25
|
+
: [];
|
|
26
|
+
}
|
|
27
|
+
case 'agentMessage':
|
|
28
|
+
return typeof item.text === 'string'
|
|
29
|
+
? [
|
|
30
|
+
{
|
|
31
|
+
id,
|
|
32
|
+
kind: 'agent',
|
|
33
|
+
text: item.text,
|
|
34
|
+
...(item.phase === 'commentary' || item.phase === 'final_answer'
|
|
35
|
+
? { phase: item.phase }
|
|
36
|
+
: {}),
|
|
37
|
+
...(timestamp ? { occurredAt: timestamp } : {}),
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
: [];
|
|
41
|
+
case 'reasoning':
|
|
42
|
+
if (!Array.isArray(item.summary))
|
|
43
|
+
return [];
|
|
44
|
+
const summary = reasoningSummary(item.summary);
|
|
45
|
+
return summary.length ? [{ id, kind: 'reasoning', summary }] : [];
|
|
46
|
+
case 'plan':
|
|
47
|
+
return typeof item.text === 'string' ? [{ id, kind: 'plan', text: item.text }] : [];
|
|
48
|
+
case 'commandExecution':
|
|
49
|
+
return typeof item.command === 'string' && typeof item.status === 'string'
|
|
50
|
+
? [
|
|
51
|
+
{
|
|
52
|
+
id,
|
|
53
|
+
kind: 'command',
|
|
54
|
+
command: item.command,
|
|
55
|
+
status: item.status,
|
|
56
|
+
...(typeof item.exitCode === 'number' ? { exitCode: item.exitCode } : {}),
|
|
57
|
+
},
|
|
58
|
+
]
|
|
59
|
+
: [];
|
|
60
|
+
default:
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function occurredAt(item, turn) {
|
|
66
|
+
const seconds = item.type === 'agentMessage' && item.phase === 'final_answer'
|
|
67
|
+
? (turn.completedAt ?? turn.startedAt)
|
|
68
|
+
: turn.startedAt;
|
|
69
|
+
return typeof seconds === 'number' ? seconds * 1_000 : undefined;
|
|
70
|
+
}
|
|
71
|
+
function isRecord(value) {
|
|
72
|
+
return typeof value === 'object' && value !== null;
|
|
73
|
+
}
|
|
74
|
+
function reasoningSummary(parts) {
|
|
75
|
+
return parts.flatMap((part) => {
|
|
76
|
+
if (typeof part === 'string')
|
|
77
|
+
return part ? [part] : [];
|
|
78
|
+
if (isRecord(part) && part.type === 'summary_text' && typeof part.text === 'string')
|
|
79
|
+
return part.text ? [part.text] : [];
|
|
80
|
+
return [];
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { buildResumeCommand } from '../application/resume-command.js';
|
|
7
|
+
export function registerGetSession(app, find) {
|
|
8
|
+
app.get('/api/sessions/:id', async (request, reply) => {
|
|
9
|
+
const session = find(request.params.id);
|
|
10
|
+
return session
|
|
11
|
+
? reply.send({
|
|
12
|
+
...session,
|
|
13
|
+
resumeCommand: session.threadId ? buildResumeCommand(session) : null,
|
|
14
|
+
})
|
|
15
|
+
: reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export function isInteractionKind(value) {
|
|
7
|
+
return ['commandApproval', 'fileChangeApproval', 'permissionsApproval', 'userInput'].includes(value);
|
|
8
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
/** Validates the relay-safe subset of Codex's generated server-request responses. */
|
|
7
|
+
export function isValidInteractionResponse(kind, value) {
|
|
8
|
+
if (!isRecord(value))
|
|
9
|
+
return false;
|
|
10
|
+
if (kind === 'userInput')
|
|
11
|
+
return isValidUserInputResponse(value);
|
|
12
|
+
if (kind === 'permissionsApproval')
|
|
13
|
+
return isRecord(value.permissions) && (value.scope === 'turn' || value.scope === 'session');
|
|
14
|
+
return isValidApprovalDecision(kind, value.decision);
|
|
15
|
+
}
|
|
16
|
+
function isValidUserInputResponse(value) {
|
|
17
|
+
if (!isRecord(value.answers))
|
|
18
|
+
return false;
|
|
19
|
+
return Object.values(value.answers).every((answer) => isRecord(answer) &&
|
|
20
|
+
Array.isArray(answer.answers) &&
|
|
21
|
+
answer.answers.every((item) => typeof item === 'string'));
|
|
22
|
+
}
|
|
23
|
+
function isValidApprovalDecision(kind, value) {
|
|
24
|
+
if (['accept', 'acceptForSession', 'decline', 'cancel'].includes(value))
|
|
25
|
+
return true;
|
|
26
|
+
if (kind !== 'commandApproval' || !isRecord(value))
|
|
27
|
+
return false;
|
|
28
|
+
return ((isRecord(value.acceptWithExecpolicyAmendment) &&
|
|
29
|
+
isRecord(value.acceptWithExecpolicyAmendment.execpolicy_amendment)) ||
|
|
30
|
+
(isRecord(value.applyNetworkPolicyAmendment) &&
|
|
31
|
+
isRecord(value.applyNetworkPolicyAmendment.network_policy_amendment)));
|
|
32
|
+
}
|
|
33
|
+
function isRecord(value) {
|
|
34
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { interruptTurn } from './use-case.js';
|
|
7
|
+
export function registerInterruptTurn(app, deps) {
|
|
8
|
+
app.post('/api/sessions/:id/turns/:turnId/interrupt', async (request, reply) => {
|
|
9
|
+
const { id, turnId } = request.params;
|
|
10
|
+
const session = deps.find(id);
|
|
11
|
+
if (!session)
|
|
12
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
13
|
+
const result = interruptTurn(session, turnId);
|
|
14
|
+
if (!result.accepted)
|
|
15
|
+
return reply.code(409).send({ code: result.code });
|
|
16
|
+
await deps.interrupt(session, turnId);
|
|
17
|
+
return reply.send({ accepted: true });
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export function interruptTurn(session, turnId) {
|
|
7
|
+
return session.activeTurnId === turnId
|
|
8
|
+
? { accepted: true }
|
|
9
|
+
: { accepted: false, code: 'TURN_NOT_ACTIVE' };
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { RelaySession } from '../model/relay-session.js';
|
|
7
|
+
export function stopSession(session, now) {
|
|
8
|
+
return RelaySession.rehydrate(session).stop(now).snapshot;
|
|
9
|
+
}
|
|
10
|
+
export function releaseSession(session, now) {
|
|
11
|
+
return RelaySession.rehydrate(session).release(now).snapshot;
|
|
12
|
+
}
|
|
13
|
+
export function restoreSession(session, now) {
|
|
14
|
+
return RelaySession.rehydrate(session).beginRecovery(now).restore(now).snapshot;
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export function registerListRecentThreads(app, deps) {
|
|
7
|
+
app.get('/api/sessions/recent-threads', async () => {
|
|
8
|
+
const threads = await deps.list();
|
|
9
|
+
return threads.map(({ id, cwd, recencyAt }) => ({ id, cwd, recencyAt }));
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { buildResumeCommand } from '../application/resume-command.js';
|
|
7
|
+
export function registerListSessions(app, deps) {
|
|
8
|
+
app.get('/api/sessions', async () => deps.list().map((session) => ({
|
|
9
|
+
...session,
|
|
10
|
+
resumeCommand: session.threadId ? buildResumeCommand(session) : null,
|
|
11
|
+
})));
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export function listSessions(sessions) {
|
|
7
|
+
return [...sessions].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
8
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export class DomainError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code) {
|
|
9
|
+
super(code);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.name = 'DomainError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { DomainError } from './errors.js';
|
|
7
|
+
import { interactionId, profileName, sessionId, threadId, turnId, workspaceId, workspacePath, } from './value-objects.js';
|
|
8
|
+
export class RelaySession {
|
|
9
|
+
value;
|
|
10
|
+
events;
|
|
11
|
+
constructor(value, events = []) {
|
|
12
|
+
this.value = value;
|
|
13
|
+
this.events = events;
|
|
14
|
+
}
|
|
15
|
+
static create(input) {
|
|
16
|
+
return new RelaySession({
|
|
17
|
+
id: sessionId(input.id),
|
|
18
|
+
workspaceId: workspaceId(input.workspaceId),
|
|
19
|
+
workspacePath: workspacePath(input.workspacePath),
|
|
20
|
+
profile: profileName(input.profile),
|
|
21
|
+
threadId: null,
|
|
22
|
+
state: 'starting',
|
|
23
|
+
desiredState: 'active',
|
|
24
|
+
activeTurnId: null,
|
|
25
|
+
protocolVersion: null,
|
|
26
|
+
failureCount: 0,
|
|
27
|
+
pendingInteractions: [],
|
|
28
|
+
createdAt: input.now,
|
|
29
|
+
updatedAt: input.now,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
static fromExistingThread(input) {
|
|
33
|
+
return new RelaySession({
|
|
34
|
+
id: sessionId(input.id),
|
|
35
|
+
workspaceId: workspaceId(input.workspaceId),
|
|
36
|
+
workspacePath: workspacePath(input.workspacePath),
|
|
37
|
+
profile: profileName(input.profile),
|
|
38
|
+
threadId: threadId(input.threadId),
|
|
39
|
+
state: 'stopped',
|
|
40
|
+
desiredState: 'stopped',
|
|
41
|
+
activeTurnId: null,
|
|
42
|
+
protocolVersion: null,
|
|
43
|
+
failureCount: 0,
|
|
44
|
+
pendingInteractions: [],
|
|
45
|
+
createdAt: input.now,
|
|
46
|
+
updatedAt: input.now,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
static rehydrate(snapshot) {
|
|
50
|
+
return new RelaySession(copy(snapshot));
|
|
51
|
+
}
|
|
52
|
+
get snapshot() {
|
|
53
|
+
return copy(this.value);
|
|
54
|
+
}
|
|
55
|
+
bindThread(value, now) {
|
|
56
|
+
return this.transition({ threadId: threadId(value), state: 'ready' }, 'ThreadBound', now);
|
|
57
|
+
}
|
|
58
|
+
startTurn(value, now) {
|
|
59
|
+
if (this.value.state === 'turnActive')
|
|
60
|
+
throw new DomainError('SESSION_TURN_ACTIVE');
|
|
61
|
+
if (this.value.state !== 'ready')
|
|
62
|
+
throw new DomainError('SESSION_NOT_READY');
|
|
63
|
+
return this.transition({ state: 'turnActive', activeTurnId: turnId(value) }, 'TurnStarted', now);
|
|
64
|
+
}
|
|
65
|
+
completeTurn(value, now) {
|
|
66
|
+
if (this.value.activeTurnId !== value)
|
|
67
|
+
throw new DomainError('TURN_NOT_ACTIVE');
|
|
68
|
+
return this.transition({ state: 'ready', activeTurnId: null }, 'TurnCompleted', now);
|
|
69
|
+
}
|
|
70
|
+
requestInteraction(input, now) {
|
|
71
|
+
if (this.value.pendingInteractions.some((item) => item.requestId === input.requestId))
|
|
72
|
+
throw new DomainError('INTERACTION_ALREADY_PENDING');
|
|
73
|
+
return this.transition({
|
|
74
|
+
pendingInteractions: [
|
|
75
|
+
...this.value.pendingInteractions,
|
|
76
|
+
{ ...input, requestId: interactionId(input.requestId) },
|
|
77
|
+
],
|
|
78
|
+
}, 'InteractionRequested', now);
|
|
79
|
+
}
|
|
80
|
+
resolveInteraction(requestId, now) {
|
|
81
|
+
if (!this.value.pendingInteractions.some((item) => item.requestId === requestId))
|
|
82
|
+
throw new DomainError('INTERACTION_NOT_PENDING');
|
|
83
|
+
return this.transition({
|
|
84
|
+
pendingInteractions: this.value.pendingInteractions.filter((item) => item.requestId !== requestId),
|
|
85
|
+
}, 'InteractionResolved', now);
|
|
86
|
+
}
|
|
87
|
+
beginRecovery(now) {
|
|
88
|
+
return this.transition({
|
|
89
|
+
state: 'recovering',
|
|
90
|
+
desiredState: 'active',
|
|
91
|
+
activeTurnId: null,
|
|
92
|
+
failureCount: this.value.failureCount + 1,
|
|
93
|
+
}, 'RecoveryBegan', now);
|
|
94
|
+
}
|
|
95
|
+
restore(now) {
|
|
96
|
+
return this.transition({ state: 'ready', desiredState: 'active', activeTurnId: null, failureCount: 0 }, 'SessionRestored', now);
|
|
97
|
+
}
|
|
98
|
+
requireAttention(now) {
|
|
99
|
+
return this.transition({ state: 'attentionRequired', activeTurnId: null }, 'AttentionRequired', now);
|
|
100
|
+
}
|
|
101
|
+
stop(now) {
|
|
102
|
+
return this.transition({ state: 'stopped', desiredState: 'stopped', activeTurnId: null }, 'SessionStopped', now);
|
|
103
|
+
}
|
|
104
|
+
release(now) {
|
|
105
|
+
return this.transition({ state: 'released', desiredState: 'stopped', activeTurnId: null }, 'SessionReleased', now);
|
|
106
|
+
}
|
|
107
|
+
transition(change, type, now) {
|
|
108
|
+
const next = {
|
|
109
|
+
...this.value,
|
|
110
|
+
...change,
|
|
111
|
+
pendingInteractions: change.pendingInteractions ?? this.value.pendingInteractions,
|
|
112
|
+
updatedAt: now,
|
|
113
|
+
};
|
|
114
|
+
return new RelaySession(next, [{ type, occurredAt: now, sessionId: this.value.id }]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function copy(snapshot) {
|
|
118
|
+
return {
|
|
119
|
+
...snapshot,
|
|
120
|
+
pendingInteractions: snapshot.pendingInteractions.map((item) => ({ ...item })),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { DomainError } from './errors.js';
|
|
7
|
+
function required(value) {
|
|
8
|
+
if (!value.trim())
|
|
9
|
+
throw new DomainError('INVALID_SESSION_VALUE');
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
export const sessionId = (value) => required(value);
|
|
13
|
+
export const workspaceId = (value) => required(value);
|
|
14
|
+
export const workspacePath = (value) => {
|
|
15
|
+
if (!value.startsWith('/'))
|
|
16
|
+
throw new DomainError('INVALID_SESSION_VALUE');
|
|
17
|
+
return required(value);
|
|
18
|
+
};
|
|
19
|
+
export const profileName = (value) => required(value);
|
|
20
|
+
export const threadId = (value) => required(value);
|
|
21
|
+
export const turnId = (value) => required(value);
|
|
22
|
+
export const interactionId = (value) => required(value);
|