@commonlyai/cli 0.1.171 → 0.1.173
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 +14 -4
- package/package.json +1 -1
- package/src/commands/agent.js +12 -6
- package/src/lib/adapters/claude.js +21 -4
- package/src/lib/adapters/codex.js +7 -2
- package/src/lib/memory-bridge.js +26 -2
- package/src/lib/handoff-memory.js +0 -23
package/README.md
CHANGED
|
@@ -33,11 +33,21 @@ Keep active-instance precedence separate from the newest-match fallback; back-to
|
|
|
33
33
|
A hosted agent's local handoff card includes `--memory-from <hosted-agent>` and
|
|
34
34
|
`--memory-instance <hosted-instance>` on the attach command. After login, attach
|
|
35
35
|
copies that source's long-term memory into the local identity before returning
|
|
36
|
-
its runnable configuration.
|
|
37
|
-
|
|
36
|
+
its runnable configuration. The source and target must belong to the logged-in
|
|
37
|
+
owner. No hosted runtime credential is moved.
|
|
38
38
|
|
|
39
39
|
This is a snapshot, not a shared identity or continuing synchronization. Attach
|
|
40
40
|
refuses to replace different existing local memory or copy into a non-private
|
|
41
|
-
long-term section.
|
|
42
|
-
|
|
41
|
+
long-term section. The install request completes memory authorization and the
|
|
42
|
+
revision-checked copy before joining the pod, creating an installation or posting
|
|
43
|
+
a greeting. A refused copy leaves those effects absent (an existing installation
|
|
44
|
+
is preserved). A later identity-creation or pod-join refusal can leave the copied
|
|
45
|
+
memory without an installation; retries recognize the snapshot as already copied.
|
|
46
|
+
The server claims the registry name before copying. On copy failure it releases
|
|
47
|
+
only the new, unchanged claim when no installation uses it; a claim touched by
|
|
48
|
+
another in-flight handoff is retained. Memory is not rolled back across later
|
|
49
|
+
identity or pod-join operations. Correct the error and
|
|
50
|
+
retry before running the local agent.
|
|
51
|
+
The CLI checks server support before publishing or installing; deploy the backend
|
|
52
|
+
with install-time handoff support before distributing the updated CLI. A retry resumes only your matching active local installation,
|
|
43
53
|
without changing its configuration or scopes. Repeating a copy with identical memory is safe.
|
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -22,7 +22,6 @@ import { createHash } from 'crypto';
|
|
|
22
22
|
|
|
23
23
|
import { requireSeatModel } from '../lib/adapters/seat-model.js';
|
|
24
24
|
import { createClient } from '../lib/api.js';
|
|
25
|
-
import { copyHandoffMemory } from '../lib/handoff-memory.js';
|
|
26
25
|
import {
|
|
27
26
|
buildSpawnId,
|
|
28
27
|
createSpawnCredentialLease,
|
|
@@ -987,6 +986,13 @@ export const performAttach = async ({
|
|
|
987
986
|
// server via legacy normalization).
|
|
988
987
|
const runtimeType = adapter.runtimeType || adapter.name;
|
|
989
988
|
|
|
989
|
+
if (memoryFrom) {
|
|
990
|
+
const capability = await client.get('/api/registry/install/memory-handoff');
|
|
991
|
+
if (capability?.installTimeMemoryHandoff !== true) {
|
|
992
|
+
throw new Error('Server does not support install-time memory handoff; upgrade the server before attaching');
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
990
996
|
// Idempotent publish — already-published is expected and fine. Install will
|
|
991
997
|
// surface the real error if the manifest is truly unusable.
|
|
992
998
|
try {
|
|
@@ -1010,7 +1016,9 @@ export const performAttach = async ({
|
|
|
1010
1016
|
// on config.environment so cross-driver implementations can realize it
|
|
1011
1017
|
// server-side without re-reading the user's file.
|
|
1012
1018
|
const installResult = await client.post('/api/registry/install', {
|
|
1013
|
-
...(memoryFrom ? {
|
|
1019
|
+
...(memoryFrom ? {
|
|
1020
|
+
resumeExisting: true, memoryFrom: { agentName: memoryFrom, instanceId: memoryInstance },
|
|
1021
|
+
} : {}),
|
|
1014
1022
|
agentName,
|
|
1015
1023
|
podId,
|
|
1016
1024
|
displayName: displayName || agentName,
|
|
@@ -1034,10 +1042,8 @@ export const performAttach = async ({
|
|
|
1034
1042
|
const instanceId = installation.instanceId || 'default';
|
|
1035
1043
|
|
|
1036
1044
|
if (memoryFrom) {
|
|
1037
|
-
const memoryCopy =
|
|
1038
|
-
|
|
1039
|
-
targetAgentName: installation.agentName, targetInstanceId: instanceId,
|
|
1040
|
-
});
|
|
1045
|
+
const memoryCopy = installResult.memoryCopy;
|
|
1046
|
+
if (!memoryCopy) throw new Error('Server does not support install-time memory handoff; upgrade the server before attaching');
|
|
1041
1047
|
log(memoryCopy.reason === 'source-empty'
|
|
1042
1048
|
? 'hosted agent has no long-term memory to copy'
|
|
1043
1049
|
: 'hosted long-term memory is present in the local agent');
|
|
@@ -81,7 +81,7 @@ import { deliverSeatCredential, withholdRuntimeCredential } from '../mcp-credent
|
|
|
81
81
|
import {
|
|
82
82
|
prepareMcpSpawn, PUBLIC_MCP_UNAVAILABLE_CODE, publicMcpUnavailableError, selectedMcpVersionDir,
|
|
83
83
|
} from '../mcp-home.js';
|
|
84
|
-
import { buildMemoryPreamble } from '../memory-bridge.js';
|
|
84
|
+
import { buildMemoryPreamble, ROW_SLICING_CUE } from '../memory-bridge.js';
|
|
85
85
|
import { claudeConfigDirFor, claudeContextTokens } from '../session-recycle.js';
|
|
86
86
|
import { skillsAsText } from '../skill-prompt.js';
|
|
87
87
|
import { adapterFailure, spawnCredentials } from '../upstream-refusal.js';
|
|
@@ -98,6 +98,16 @@ const DEFAULT_TIMEOUT_MS = (() => {
|
|
|
98
98
|
})();
|
|
99
99
|
|
|
100
100
|
const buildPrompt = buildMemoryPreamble;
|
|
101
|
+
|
|
102
|
+
// The working habits a private claude seat is taught on a fresh session (D48,
|
|
103
|
+
// TASK-545). Parallel reads live here because only this CLI has a subagent
|
|
104
|
+
// tool; codex is passed row slicing alone. A public seat is passed neither: its
|
|
105
|
+
// argv allows Read and the MCP under dontAsk, so it has no subagent to spawn,
|
|
106
|
+
// and it answers strangers rather than working board rows.
|
|
107
|
+
const PARALLEL_READS_CUE = 'Parallel reads: when a row needs several files or sources read, '
|
|
108
|
+
+ 'use the Agent tool. Spawn read-only subagents in parallel, brief each one with the files '
|
|
109
|
+
+ 'to read and the question to answer, and keep only their conclusions.';
|
|
110
|
+
const WORK_CUES = [PARALLEL_READS_CUE, ROW_SLICING_CUE];
|
|
101
111
|
const RUNTIME_CREDENTIAL_PLACEHOLDER = '${COMMONLY_AGENT_TOKEN}';
|
|
102
112
|
|
|
103
113
|
const QUOTE_SHELL_ARGUMENT = (value) => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
@@ -799,7 +809,12 @@ export default {
|
|
|
799
809
|
// the only two paths that build a real prompt. buildPrompt handles
|
|
800
810
|
// undefined and '' as absence itself; it does not need a guard, it needs
|
|
801
811
|
// the value.
|
|
802
|
-
const
|
|
812
|
+
const publicTrust = ctx.environment?.sandbox?.trust === 'public';
|
|
813
|
+
const workCues = publicTrust ? [] : WORK_CUES;
|
|
814
|
+
const fullPrompt = buildPrompt(prompt, ctx.memoryLongTerm, {
|
|
815
|
+
freshSession: !isResume,
|
|
816
|
+
workCues,
|
|
817
|
+
});
|
|
803
818
|
const sessionFlag = isResume ? '--resume' : '--session-id';
|
|
804
819
|
// Model pin from the ADR-008 environment spec. Absent it, claude picks its
|
|
805
820
|
// own default — which is how a fleet of ten agents ended up running three
|
|
@@ -839,7 +854,6 @@ export default {
|
|
|
839
854
|
let mcpConfig = null;
|
|
840
855
|
let publicClaudeState = null;
|
|
841
856
|
let publicSkillArgs = [];
|
|
842
|
-
const publicTrust = ctx.environment?.sandbox?.trust === 'public';
|
|
843
857
|
const publicNativeSandbox = publicTrust
|
|
844
858
|
&& PUBLIC_SANDBOX_MODES.has(resolvePublicSandboxMode(ctx.environment?.sandbox));
|
|
845
859
|
const publicBwrapSandbox = publicTrust
|
|
@@ -927,7 +941,10 @@ export default {
|
|
|
927
941
|
// The retry creates a new underlying CLI session. Rebuild its prompt
|
|
928
942
|
// as fresh too: otherwise a session-recovery path is the one fresh
|
|
929
943
|
// session that misses the durable-state cue.
|
|
930
|
-
const freshPrompt = buildPrompt(prompt, ctx.memoryLongTerm, {
|
|
944
|
+
const freshPrompt = buildPrompt(prompt, ctx.memoryLongTerm, {
|
|
945
|
+
freshSession: true,
|
|
946
|
+
workCues,
|
|
947
|
+
});
|
|
931
948
|
const retryBase = ['-p', freshPrompt,
|
|
932
949
|
'--output-format', 'text', '--session-id', freshId, ...modelArgs];
|
|
933
950
|
const retryCtx = {
|
|
@@ -66,7 +66,7 @@ import { CREDENTIAL_KEY, writeCredentialFile } from '../credential-file.js';
|
|
|
66
66
|
import { supportedGitHooks } from '../commit-attribution.js';
|
|
67
67
|
import { deliverSeatCredential, withholdRuntimeCredential } from '../mcp-credential-delivery.js';
|
|
68
68
|
import { prepareMcpSpawn, publicMcpUnavailableError, selectedMcpVersionDir } from '../mcp-home.js';
|
|
69
|
-
import { buildMemoryPreamble } from '../memory-bridge.js';
|
|
69
|
+
import { buildMemoryPreamble, ROW_SLICING_CUE } from '../memory-bridge.js';
|
|
70
70
|
import { resolveSkillSource, skillsAsText } from '../skill-prompt.js';
|
|
71
71
|
import { isLegacySandboxTrust, normalizeSandboxTrust } from '../environment.js';
|
|
72
72
|
import { adapterFailure, spawnCredentials } from '../upstream-refusal.js';
|
|
@@ -650,14 +650,20 @@ export default {
|
|
|
650
650
|
|
|
651
651
|
async spawn(prompt, ctx = {}) {
|
|
652
652
|
const seatModel = requireSeatModel(ctx.environment, 'codex');
|
|
653
|
+
const sandbox = normalizeSandboxTrust(ctx.environment?.sandbox);
|
|
653
654
|
// Passed through UNCOALESCED. `ctx.memoryLongTerm || ''` was here, and
|
|
654
655
|
// `null || ''` is `''` — which routed the unreadable case straight into
|
|
655
656
|
// the empty-memory branch and made the whole distinction unreachable from
|
|
656
657
|
// the only two paths that build a real prompt. buildPrompt handles
|
|
657
658
|
// undefined and '' as absence itself; it does not need a guard, it needs
|
|
658
659
|
// the value.
|
|
660
|
+
//
|
|
661
|
+
// Codex has no subagent tool, so of D48's two habits (TASK-545) it is
|
|
662
|
+
// taught row slicing alone; claude.js adds parallel reads. A public seat
|
|
663
|
+
// answers strangers rather than working board rows, so it is taught neither.
|
|
659
664
|
const fullPrompt = buildPrompt(prompt, ctx.memoryLongTerm, {
|
|
660
665
|
freshSession: !ctx.sessionId,
|
|
666
|
+
workCues: sandbox?.trust === 'public' ? [] : [ROW_SLICING_CUE],
|
|
661
667
|
});
|
|
662
668
|
|
|
663
669
|
// Per-spawn temp dir for --output-last-message. Cleaned up in `finally`
|
|
@@ -674,7 +680,6 @@ export default {
|
|
|
674
680
|
});
|
|
675
681
|
|
|
676
682
|
try {
|
|
677
|
-
const sandbox = normalizeSandboxTrust(ctx.environment?.sandbox);
|
|
678
683
|
const mcp = buildMcpOverrideArgs(ctx.environment?.mcp, {
|
|
679
684
|
runtimeToken: ctx.runtimeToken,
|
|
680
685
|
instanceUrl: ctx.instanceUrl,
|
package/src/lib/memory-bridge.js
CHANGED
|
@@ -22,6 +22,18 @@
|
|
|
22
22
|
|
|
23
23
|
export const SOURCE_RUNTIME = 'local-cli';
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Row slicing, the runtime-agnostic half of D48 (TASK-545). A row too large
|
|
27
|
+
* for one turn drifts when one seat carries it whole, so the seat splits it on
|
|
28
|
+
* the board before any code. It names the exact call and field for the reason
|
|
29
|
+
* the memory cue below names `long_term`: a seat adopts a habit through a
|
|
30
|
+
* surface it can see. Each adapter decides whether to pass it.
|
|
31
|
+
*/
|
|
32
|
+
export const ROW_SLICING_CUE = 'Slicing: when a row is too large for one turn, split it before '
|
|
33
|
+
+ 'writing any code. Post each slice as a child row (commonly_create_task with parentTask set '
|
|
34
|
+
+ "to the row's taskId) with one assignee, a written file or area boundary, and its own "
|
|
35
|
+
+ 'acceptance line.';
|
|
36
|
+
|
|
25
37
|
/**
|
|
26
38
|
* The turn preamble, shared by every adapter so the two cannot drift.
|
|
27
39
|
*
|
|
@@ -39,8 +51,17 @@ export const SOURCE_RUNTIME = 'local-cli';
|
|
|
39
51
|
* the section whose name most invites exactly this use — succeeds, returns 200,
|
|
40
52
|
* and is never seen again; the write and the silence are indistinguishable from
|
|
41
53
|
* a correct round trip. Naming the section is the whole point of the cue.
|
|
54
|
+
*
|
|
55
|
+
* `workCues` are working habits an adapter teaches on a fresh session, beside
|
|
56
|
+
* the memory cues and under the same once-per-session rule. An adapter passes
|
|
57
|
+
* only the habits its CLI can act on; passing none leaves the preamble as it
|
|
58
|
+
* was.
|
|
42
59
|
*/
|
|
43
|
-
export const buildMemoryPreamble = (
|
|
60
|
+
export const buildMemoryPreamble = (
|
|
61
|
+
prompt,
|
|
62
|
+
memoryLongTerm,
|
|
63
|
+
{ freshSession = false, workCues = [] } = {},
|
|
64
|
+
) => {
|
|
44
65
|
// `null` is the UNREADABLE signal, and it is deliberately not the same value
|
|
45
66
|
// as `''`. Telling a seat whose token was revoked that "nothing has ever been
|
|
46
67
|
// saved here" is a false claim about its own history, and it is the same
|
|
@@ -87,7 +108,10 @@ export const buildMemoryPreamble = (prompt, memoryLongTerm, { freshSession = fal
|
|
|
87
108
|
+ 'gates held, decisions pending, and task context, not a transcript — using '
|
|
88
109
|
+ 'the long_term write above.';
|
|
89
110
|
}
|
|
90
|
-
|
|
111
|
+
const workCue = workCues.length
|
|
112
|
+
? `\n=== Working a row ===\n${workCues.map((cue) => `- ${cue}`).join('\n')}\n`
|
|
113
|
+
: '';
|
|
114
|
+
return `${context}\n${freshReadCue}${workCue}\n=== Current turn ===\n${prompt}\n${sessionEndCue}`;
|
|
91
115
|
};
|
|
92
116
|
|
|
93
117
|
export const readLongTerm = async (client, { onError } = {}) => {
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/** Copy only long-term memory using the logged-in owner's revision-checked API. */
|
|
2
|
-
export const copyHandoffMemory = async (client, {
|
|
3
|
-
sourceAgentName, sourceInstanceId = 'default', targetAgentName, targetInstanceId = 'default',
|
|
4
|
-
}) => {
|
|
5
|
-
const endpoint = (name, instance) => `/api/agent-memory/${encodeURIComponent(name)}/${encodeURIComponent(instance)}/editable`;
|
|
6
|
-
const source = await client.get(endpoint(sourceAgentName, sourceInstanceId));
|
|
7
|
-
const content = source?.sections?.long_term?.content;
|
|
8
|
-
if (typeof content !== 'string') throw new Error('Hosted memory response is missing long_term content');
|
|
9
|
-
if (!content) return { copied: false, reason: 'source-empty' };
|
|
10
|
-
const targetPath = endpoint(targetAgentName, targetInstanceId);
|
|
11
|
-
const target = await client.get(targetPath);
|
|
12
|
-
const prior = target?.sections?.long_term;
|
|
13
|
-
if (typeof prior?.content !== 'string') throw new Error('Local memory response is missing long_term content');
|
|
14
|
-
if (prior.visibility !== 'private') throw new Error('Local long-term memory must be private before copying hosted memory');
|
|
15
|
-
if (prior.content === content) return { copied: false, reason: 'already-copied' };
|
|
16
|
-
if (prior.content) throw new Error('Local agent already has different memory; choose a new local name');
|
|
17
|
-
// A concurrent first write wins rather than being overwritten by this copy.
|
|
18
|
-
await client.patch(`${targetPath}/long_term`, {
|
|
19
|
-
copyFrom: { agentName: sourceAgentName, instanceId: sourceInstanceId },
|
|
20
|
-
expectedUpdatedAt: prior.updatedAt ?? null,
|
|
21
|
-
});
|
|
22
|
-
return { copied: true };
|
|
23
|
-
};
|