synorch 0.4.0-beta.0 → 0.4.0-beta.1
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/CHANGELOG.md +13 -1
- package/README.md +2 -2
- package/dist/build-info.json +2 -2
- package/dist/domain/product.d.ts +1 -1
- package/dist/domain/product.js +1 -1
- package/dist/harness/auth/json-file.d.ts +8 -0
- package/dist/harness/auth/json-file.d.ts.map +1 -1
- package/dist/harness/auth/json-file.js +17 -6
- package/dist/harness/auth/json-file.js.map +1 -1
- package/dist/harness/auth/locks.d.ts +5 -0
- package/dist/harness/auth/locks.d.ts.map +1 -1
- package/dist/harness/auth/locks.js +51 -22
- package/dist/harness/auth/locks.js.map +1 -1
- package/dist/harness/cli/conversation.d.ts.map +1 -1
- package/dist/harness/cli/conversation.js +106 -13
- package/dist/harness/cli/conversation.js.map +1 -1
- package/dist/harness/cli/first-run-connect.d.ts +43 -0
- package/dist/harness/cli/first-run-connect.d.ts.map +1 -0
- package/dist/harness/cli/first-run-connect.js +144 -0
- package/dist/harness/cli/first-run-connect.js.map +1 -0
- package/dist/harness/cli/runtime.d.ts.map +1 -1
- package/dist/harness/cli/runtime.js +2 -0
- package/dist/harness/cli/runtime.js.map +1 -1
- package/dist/harness/contracts/renderer.d.ts +2 -0
- package/dist/harness/contracts/renderer.d.ts.map +1 -1
- package/dist/harness/contracts/renderer.js.map +1 -1
- package/dist/harness/tui/pi-tui-renderer.d.ts.map +1 -1
- package/dist/harness/tui/pi-tui-renderer.js +1 -0
- package/dist/harness/tui/pi-tui-renderer.js.map +1 -1
- package/dist/infrastructure/build-info.d.ts +1 -1
- package/dist/infrastructure/build-info.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0-beta.1] - 2026-09-26
|
|
11
|
+
|
|
12
|
+
Pre-release under the npm dist-tag `beta`. `latest` stays on 0.3.0.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- First run on a new machine: bare `syn` asks which provider to connect, signs in, saves `routes.session` and continues, instead of exiting with `config_invalid` before any sign-in was possible. Headless runs without a route name the two commands to run.
|
|
17
|
+
- A turn that fails because the provider is not signed in offers to sign in in the session and retry.
|
|
18
|
+
- A Claude Code sign-in made inside a session is visible to it immediately.
|
|
19
|
+
- Windows: the credential refresh lock tolerates `EPERM`/`EBUSY`.
|
|
20
|
+
|
|
10
21
|
## [0.4.0-beta.0] - 2026-09-26
|
|
11
22
|
|
|
12
23
|
Pre-release, published under the npm dist-tag `beta` (`npm i -g synorch@beta`). `latest` stays on 0.3.0.
|
|
@@ -88,7 +99,8 @@ Pre-release, published under the npm dist-tag `beta` (`npm i -g synorch@beta`).
|
|
|
88
99
|
- A bundled, provenance-tracked Ingenium skill catalog with on-demand loading rules.
|
|
89
100
|
- Model profiles, orchestration protocols, minimal context packets, verification contracts, and provider adapters.
|
|
90
101
|
|
|
91
|
-
[Unreleased]: https://github.com/TunahanTuna/synorch/compare/v0.4.0-beta.
|
|
102
|
+
[Unreleased]: https://github.com/TunahanTuna/synorch/compare/v0.4.0-beta.1...HEAD
|
|
103
|
+
[0.4.0-beta.1]: https://github.com/TunahanTuna/synorch/compare/v0.4.0-beta.0...v0.4.0-beta.1
|
|
92
104
|
[0.4.0-beta.0]: https://github.com/TunahanTuna/synorch/compare/v0.3.0...v0.4.0-beta.0
|
|
93
105
|
[0.3.0]: https://github.com/TunahanTuna/synorch/compare/v0.2.0...v0.3.0
|
|
94
106
|
[0.2.0]: https://github.com/TunahanTuna/synorch/compare/v0.1.0...v0.2.0
|
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Installing from a local checkout (works on Windows, macOS and Linux; no `syn ini
|
|
|
70
70
|
```bash
|
|
71
71
|
pnpm install
|
|
72
72
|
pnpm run install:global # build, pack and install a self-contained global copy
|
|
73
|
-
syn --version # 0.4.0-beta.
|
|
73
|
+
syn --version # 0.4.0-beta.1 (<commit>)
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
See [docs/harness/delivery/install.md](./docs/harness/delivery/install.md) for the linked (live-rebuild) variant, updating and uninstalling.
|
|
@@ -353,7 +353,7 @@ pnpm dev -- inspect # run the CLI from source
|
|
|
353
353
|
| [Architecture](./docs/AI-ORCHESTRATION-ARCHITECTURE.md) | The full design: orchestration model, protocol system, context architecture, model routing, discovery, open decisions, and the dated decision log. |
|
|
354
354
|
| [Multi-provider harness vision](./docs/FUTURE-MULTI-PROVIDER-HARNESS.md) | Where this goes next — running different providers in different roles under one orchestrator. Proposal stage, not implemented. |
|
|
355
355
|
| [CHANGELOG](./CHANGELOG.md) | Version history, Keep a Changelog + SemVer. |
|
|
356
|
-
| [Release notes v0.4.0-beta.
|
|
356
|
+
| [Release notes v0.4.0-beta.1](./docs/releases/v0.4.0-beta.1.md) | Human-readable notes for the current beta (`synorch@beta`). |
|
|
357
357
|
| [Release notes v0.3.0](./docs/releases/v0.3.0.md) | Human-readable notes for the current stable release. |
|
|
358
358
|
| [Security policy](./SECURITY.md) | Report vulnerabilities through the private channel, not a public issue. |
|
|
359
359
|
|
package/dist/build-info.json
CHANGED
package/dist/domain/product.d.ts
CHANGED
package/dist/domain/product.js
CHANGED
|
@@ -5,4 +5,12 @@ export declare function readJsonFile(filePath: string): Promise<unknown>;
|
|
|
5
5
|
* Windows the mode bits are ignored and the file inherits the user profile's ACL.
|
|
6
6
|
*/
|
|
7
7
|
export declare function writeJsonAtomic(filePath: string, value: unknown): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Windows reports EPERM/EACCES/EBUSY while another handle (a concurrent reader, antivirus, the
|
|
10
|
+
* indexer) holds a file, or while it is still delete-pending after an unlink. Those clear within
|
|
11
|
+
* milliseconds, so the operation is retried with a bounded exponential backoff (about 1.3s in
|
|
12
|
+
* total at the default 8 attempts) before the error is surfaced.
|
|
13
|
+
*/
|
|
14
|
+
export declare function retryTransientFs<T>(operation: () => Promise<T>, attempts?: number): Promise<T>;
|
|
15
|
+
export declare function isTransientFsError(error: unknown): boolean;
|
|
8
16
|
//# sourceMappingURL=json-file.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-file.d.ts","sourceRoot":"","sources":["../../../src/harness/auth/json-file.ts"],"names":[],"mappings":"AAIA,uHAAuH;AACvH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CASrE;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBrF"}
|
|
1
|
+
{"version":3,"file":"json-file.d.ts","sourceRoot":"","sources":["../../../src/harness/auth/json-file.ts"],"names":[],"mappings":"AAIA,uHAAuH;AACvH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CASrE;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBrF;AAQD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,QAAQ,SAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAS/F;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D"}
|
|
@@ -41,17 +41,28 @@ export async function writeJsonAtomic(filePath, value) {
|
|
|
41
41
|
await chmod(filePath, 0o600);
|
|
42
42
|
}
|
|
43
43
|
async function renameWithRetry(from, to) {
|
|
44
|
-
|
|
44
|
+
await retryTransientFs(() => rename(from, to));
|
|
45
|
+
}
|
|
46
|
+
const TRANSIENT_FS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
47
|
+
/**
|
|
48
|
+
* Windows reports EPERM/EACCES/EBUSY while another handle (a concurrent reader, antivirus, the
|
|
49
|
+
* indexer) holds a file, or while it is still delete-pending after an unlink. Those clear within
|
|
50
|
+
* milliseconds, so the operation is retried with a bounded exponential backoff (about 1.3s in
|
|
51
|
+
* total at the default 8 attempts) before the error is surfaced.
|
|
52
|
+
*/
|
|
53
|
+
export async function retryTransientFs(operation, attempts = 8) {
|
|
54
|
+
for (let attempt = 1;; attempt += 1) {
|
|
45
55
|
try {
|
|
46
|
-
await
|
|
47
|
-
return;
|
|
56
|
+
return await operation();
|
|
48
57
|
}
|
|
49
58
|
catch (error) {
|
|
50
|
-
|
|
51
|
-
if (attempt >= 10 || (code !== "EPERM" && code !== "EACCES" && code !== "EBUSY"))
|
|
59
|
+
if (attempt >= attempts || !isTransientFsError(error))
|
|
52
60
|
throw error;
|
|
53
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, 5 * 2 ** attempt));
|
|
54
62
|
}
|
|
55
63
|
}
|
|
56
64
|
}
|
|
65
|
+
export function isTransientFsError(error) {
|
|
66
|
+
return TRANSIENT_FS_CODES.has(error?.code ?? "");
|
|
67
|
+
}
|
|
57
68
|
//# sourceMappingURL=json-file.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-file.js","sourceRoot":"","sources":["../../../src/harness/auth/json-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,uHAAuH;AACvH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC1C,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACzE,MAAM,KAAK,CAAC;IACd,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;AACrC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAgB,EAAE,KAAc;IACpE,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzC,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3H,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IAClD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACtE,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;IACtB,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IACD,IAAI,CAAC;QACH,MAAM,eAAe,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC5D,MAAM,KAAK,CAAC;IACd,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,MAAM,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;AACjE,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,IAAY,EAAE,EAAU;IACrD,
|
|
1
|
+
{"version":3,"file":"json-file.js","sourceRoot":"","sources":["../../../src/harness/auth/json-file.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,6 +9,11 @@ export interface FileLockOptions {
|
|
|
9
9
|
* Cross-process mutual exclusion through an `O_EXCL` lock file holding the owner's pid and a random
|
|
10
10
|
* token. Release removes the file only if it still carries our token. A lock whose holder process
|
|
11
11
|
* is gone, or which is older than `staleMs`, is taken over.
|
|
12
|
+
*
|
|
13
|
+
* On Windows a just-released lock file stays delete-pending while any other handle (a waiter's
|
|
14
|
+
* stale check, antivirus, the indexer) is still open on it; during that window `open(wx)`, `unlink`
|
|
15
|
+
* and reads fail with EPERM/EACCES/EBUSY. On `open` that means "someone is on it" and is treated
|
|
16
|
+
* like EEXIST (keep polling); `unlink` and the token read are retried with a bounded backoff.
|
|
12
17
|
*/
|
|
13
18
|
export declare function acquireFileLock(lockPath: string, options?: FileLockOptions): Promise<() => Promise<void>>;
|
|
14
19
|
/** In-process, per-key serialization: callers for the same key run one after another. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locks.d.ts","sourceRoot":"","sources":["../../../src/harness/auth/locks.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"locks.d.ts","sourceRoot":"","sources":["../../../src/harness/auth/locks.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,eAAe;IAC9B,sFAAsF;IACtF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAMD;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAgCnH;AA0DD,yFAAyF;AACzF,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuC;IAEhD,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAWrE"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { mkdir, open, readFile,
|
|
2
|
+
import { mkdir, open, readFile, stat, unlink } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { HarnessError } from "../contracts/index.js";
|
|
5
|
+
import { isTransientFsError, retryTransientFs } from "./json-file.js";
|
|
5
6
|
const DEFAULT_STALE_MS = 60_000;
|
|
6
7
|
const DEFAULT_TIMEOUT_MS = 90_000;
|
|
7
8
|
const DEFAULT_POLL_MS = 25;
|
|
@@ -9,6 +10,11 @@ const DEFAULT_POLL_MS = 25;
|
|
|
9
10
|
* Cross-process mutual exclusion through an `O_EXCL` lock file holding the owner's pid and a random
|
|
10
11
|
* token. Release removes the file only if it still carries our token. A lock whose holder process
|
|
11
12
|
* is gone, or which is older than `staleMs`, is taken over.
|
|
13
|
+
*
|
|
14
|
+
* On Windows a just-released lock file stays delete-pending while any other handle (a waiter's
|
|
15
|
+
* stale check, antivirus, the indexer) is still open on it; during that window `open(wx)`, `unlink`
|
|
16
|
+
* and reads fail with EPERM/EACCES/EBUSY. On `open` that means "someone is on it" and is treated
|
|
17
|
+
* like EEXIST (keep polling); `unlink` and the token read are retried with a bounded backoff.
|
|
12
18
|
*/
|
|
13
19
|
export async function acquireFileLock(lockPath, options = {}) {
|
|
14
20
|
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
@@ -19,35 +25,24 @@ export async function acquireFileLock(lockPath, options = {}) {
|
|
|
19
25
|
if (options.signal?.aborted === true) {
|
|
20
26
|
throw new HarnessError({ code: "cancelled", message: "lock wait aborted", workspace_effect: "none", retry_safe: true });
|
|
21
27
|
}
|
|
28
|
+
let transient;
|
|
22
29
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
await handle.writeFile(JSON.stringify({ pid: process.pid, token, acquired_at: new Date().toISOString() }));
|
|
26
|
-
await handle.sync();
|
|
27
|
-
}
|
|
28
|
-
finally {
|
|
29
|
-
await handle.close();
|
|
30
|
-
}
|
|
31
|
-
return async () => {
|
|
32
|
-
try {
|
|
33
|
-
const current = JSON.parse(await readFile(lockPath, "utf8"));
|
|
34
|
-
if (current.token === token)
|
|
35
|
-
await rm(lockPath, { force: true });
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
30
|
+
await createLockFile(lockPath, token);
|
|
31
|
+
return () => releaseFileLock(lockPath, token);
|
|
41
32
|
}
|
|
42
33
|
catch (error) {
|
|
43
|
-
if (
|
|
34
|
+
if (isTransientFsError(error))
|
|
35
|
+
transient = error;
|
|
36
|
+
else if (!isExists(error))
|
|
44
37
|
throw error;
|
|
45
38
|
}
|
|
46
|
-
if (await isAbandoned(lockPath, staleMs)) {
|
|
47
|
-
await
|
|
39
|
+
if (transient === undefined && (await isAbandoned(lockPath, staleMs))) {
|
|
40
|
+
await removeLockFile(lockPath).catch(() => undefined);
|
|
48
41
|
continue;
|
|
49
42
|
}
|
|
50
43
|
if (Date.now() > deadline) {
|
|
44
|
+
if (transient !== undefined)
|
|
45
|
+
throw transient;
|
|
51
46
|
throw new HarnessError({
|
|
52
47
|
code: "internal",
|
|
53
48
|
message: `timed out waiting for lock ${path.basename(lockPath)}`,
|
|
@@ -58,6 +53,40 @@ export async function acquireFileLock(lockPath, options = {}) {
|
|
|
58
53
|
await new Promise((resolve) => setTimeout(resolve, options.pollMs ?? DEFAULT_POLL_MS));
|
|
59
54
|
}
|
|
60
55
|
}
|
|
56
|
+
/** Creates the lock exclusively; a failed write never leaves a half-written lock file behind. */
|
|
57
|
+
async function createLockFile(lockPath, token) {
|
|
58
|
+
const handle = await open(lockPath, "wx", 0o600);
|
|
59
|
+
try {
|
|
60
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, token, acquired_at: new Date().toISOString() }));
|
|
61
|
+
await handle.sync();
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
await handle.close().catch(() => undefined);
|
|
65
|
+
await removeLockFile(lockPath).catch(() => undefined);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
await handle.close();
|
|
69
|
+
}
|
|
70
|
+
/** Removes the lock only while it still carries our token. Never throws: release runs in `finally` blocks. */
|
|
71
|
+
async function releaseFileLock(lockPath, token) {
|
|
72
|
+
try {
|
|
73
|
+
const current = JSON.parse(await retryTransientFs(() => readFile(lockPath, "utf8")));
|
|
74
|
+
if (current.token === token)
|
|
75
|
+
await removeLockFile(lockPath, 10);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function removeLockFile(lockPath, attempts) {
|
|
82
|
+
try {
|
|
83
|
+
await retryTransientFs(() => unlink(lockPath), attempts);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error.code !== "ENOENT")
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
61
90
|
async function isAbandoned(lockPath, staleMs) {
|
|
62
91
|
try {
|
|
63
92
|
const [info, text] = await Promise.all([stat(lockPath), readFile(lockPath, "utf8")]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locks.js","sourceRoot":"","sources":["../../../src/harness/auth/locks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"locks.js","sourceRoot":"","sources":["../../../src/harness/auth/locks.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../../src/harness/cli/conversation.ts"],"names":[],"mappings":"AAKA,OAAO,
|
|
1
|
+
{"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../../src/harness/cli/conversation.ts"],"names":[],"mappings":"AAKA,OAAO,EA2BL,KAAK,eAAe,EAsBrB,MAAM,uBAAuB,CAAC;AAwB/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AA2B/C,OAAO,EAA+B,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAClF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAsB9C;;;;;;;;;;;;;;;;;GAiBG;AAEH,KAAK,YAAY,GAAG,OAAO,CAAC,aAAa,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AAyG9D,4GAA4G;AAC5G,wBAAgB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe,CAMrE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe,CAMpE;AA2ED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAE3H"}
|
|
@@ -32,6 +32,7 @@ import { defaultEffort, isReasoningEffort } from "../providers/index.js";
|
|
|
32
32
|
import { openBrowser } from "../tui/open-browser.js";
|
|
33
33
|
import { acknowledgedNotices, acknowledgeNotice, approvalNotice, mcpStartNotices, StartupNotices } from "./startup-notices.js";
|
|
34
34
|
import { failureInfo } from "./outcome.js";
|
|
35
|
+
import { connectProvider, MANUAL_CONNECT_COMMANDS, signIn } from "./first-run-connect.js";
|
|
35
36
|
import { createSessionRenderer } from "./renderers.js";
|
|
36
37
|
import { createRuntime } from "./runtime.js";
|
|
37
38
|
import { changePathspecs, commitAll, commitSelected, commitsSince, reviewDiff, uncommittedChanges } from "./session-git.js";
|
|
@@ -130,10 +131,10 @@ function missingRoute(runtime) {
|
|
|
130
131
|
const hints = profileHintsFor(runtime.canonical, "orchestrator").map((hint) => `${hint.provider}/${hint.model}`);
|
|
131
132
|
return new HarnessError({
|
|
132
133
|
code: "config_invalid",
|
|
133
|
-
message: `no model
|
|
134
|
+
message: `no model is connected for the conversation: run ${MANUAL_CONNECT_COMMANDS} (or start syn in an interactive terminal to sign in there; --profile session=<provider>/<model> sets one for a single run)${hints.length === 0 ? "" : `; the canonical model profiles suggest ${hints.join(" or ")}`}`,
|
|
134
135
|
workspace_effect: "none",
|
|
135
136
|
retry_safe: true,
|
|
136
|
-
next_command: "syn
|
|
137
|
+
next_command: "syn login openai && syn config set routes.session openai/gpt-6-sol",
|
|
137
138
|
});
|
|
138
139
|
}
|
|
139
140
|
/** The conversation model the user saved with `/model <tier> --save` (a preference, not a route). */
|
|
@@ -218,6 +219,8 @@ class Conversation {
|
|
|
218
219
|
grantList = [];
|
|
219
220
|
routePromise;
|
|
220
221
|
routeRecorded = false;
|
|
222
|
+
/** The current turn's model request failed for want of a signed-in identity (a sign-in is offered after it). */
|
|
223
|
+
authFailure = false;
|
|
221
224
|
sessionTier;
|
|
222
225
|
currentModel;
|
|
223
226
|
sessionId;
|
|
@@ -345,14 +348,12 @@ class Conversation {
|
|
|
345
348
|
if (runtime.permissionMode() === "plan")
|
|
346
349
|
this.modeBeforePlan = startMode === "plan" ? DEFAULT_PERMISSION_MODE : startMode;
|
|
347
350
|
this.sessionTier = await savedSessionTier(runtime.home);
|
|
348
|
-
|
|
349
|
-
|
|
351
|
+
let rule = sessionRouteRule(runtime, this.sessionTier);
|
|
352
|
+
// First run without a route: an interactive terminal connects a provider after the view starts; headless fails with the commands.
|
|
353
|
+
if (rule === undefined && !this.canConnect())
|
|
350
354
|
return this.fail(failureInfo(missingRoute(runtime)));
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
// Resolving the route may probe the provider's capabilities: it runs while the user types.
|
|
354
|
-
this.routePromise = runtime.router.resolve({ tier: rule.tier, role: "session" }, this.outer.signal);
|
|
355
|
-
this.routePromise.catch(() => undefined);
|
|
355
|
+
if (rule !== undefined)
|
|
356
|
+
this.adoptRule(rule);
|
|
356
357
|
const ledger = new UsageLedger(runtime.home);
|
|
357
358
|
this.usageLedger = ledger;
|
|
358
359
|
const unsubscribe = runtime.subscribe((event) => {
|
|
@@ -377,10 +378,23 @@ class Conversation {
|
|
|
377
378
|
const resumed = await this.openResumed();
|
|
378
379
|
const sandboxNoticeSeen = runtime.sandbox.enforcement === "full" || (await acknowledgedNotices(runtime.home)).has("sandbox-partial");
|
|
379
380
|
this.notices = new StartupNotices((notice) => this.note(notice.level, notice.text), this.glyphs.sep);
|
|
380
|
-
|
|
381
|
+
const hint = await this.welcomeHint(resumed !== undefined);
|
|
382
|
+
await this.renderer.start({ ...(rule === undefined ? this.unconnectedHeader(hint) : this.header(rule, hint)), sandboxNoticeSeen });
|
|
381
383
|
// The partial-sandbox warning shows once per machine; /permissions keeps it.
|
|
382
384
|
if (!sandboxNoticeSeen && this.renderer.controls?.appearance !== undefined)
|
|
383
385
|
void acknowledgeNotice(runtime.home, "sandbox-partial");
|
|
386
|
+
if (rule === undefined) {
|
|
387
|
+
const connected = await connectProvider(this.connectHost());
|
|
388
|
+
rule = connected === undefined ? undefined : sessionRouteRule(runtime, "session");
|
|
389
|
+
if (rule === undefined) {
|
|
390
|
+
this.note("info", `No provider connected ${this.glyphs.sep} to set one up by hand: ${MANUAL_CONNECT_COMMANDS}`);
|
|
391
|
+
await this.renderer.stop("completed");
|
|
392
|
+
return EXIT_CODES.success;
|
|
393
|
+
}
|
|
394
|
+
this.adoptRule(rule);
|
|
395
|
+
const view = this.header(rule).welcome;
|
|
396
|
+
this.renderer.controls?.appearance?.updateWelcome({ model: rule.route.model_id, ...(view?.effort === undefined ? {} : { effort: view.effort }), ...(view?.plan === undefined ? {} : { plan: view.plan }) });
|
|
397
|
+
}
|
|
384
398
|
const controls = this.renderer.controls;
|
|
385
399
|
this.refreshPalette();
|
|
386
400
|
this.refreshStatus();
|
|
@@ -416,7 +430,7 @@ class Conversation {
|
|
|
416
430
|
else
|
|
417
431
|
void Promise.race([this.mcpStarted, delay(1500)]).then(() => this.notices?.flush());
|
|
418
432
|
// Credential pre-resolution (keychain, token refresh) happens in the background, never before the editor.
|
|
419
|
-
void this.routePromise
|
|
433
|
+
void this.routePromise?.then((decision) => runtime.credentials(decision.route, this.outer.signal)).catch(() => undefined);
|
|
420
434
|
const input = this.renderer.input;
|
|
421
435
|
if (input === undefined) {
|
|
422
436
|
this.note("warning", "syn agent needs input: attach a terminal or pipe messages on stdin, one per line");
|
|
@@ -455,6 +469,71 @@ class Conversation {
|
|
|
455
469
|
this.io.stderr.write(formatHarnessError(error));
|
|
456
470
|
return exitCodeFor(error.code);
|
|
457
471
|
}
|
|
472
|
+
/** A human at a terminal who can pick and sign in to a provider (first run, or a turn that needs sign-in). */
|
|
473
|
+
canConnect() {
|
|
474
|
+
return this.io.stdinIsTTY && this.renderer.input !== undefined && this.renderer.auth.interactive;
|
|
475
|
+
}
|
|
476
|
+
adoptRule(rule) {
|
|
477
|
+
this.sessionTier = rule.tier;
|
|
478
|
+
this.currentModel = rule.route.model_id;
|
|
479
|
+
// Resolving the route may probe the provider's capabilities: it runs while the user types.
|
|
480
|
+
this.routePromise = this.runtime.router.resolve({ tier: rule.tier, role: "session" }, this.outer.signal);
|
|
481
|
+
this.routePromise.catch(() => undefined);
|
|
482
|
+
this.routeRecorded = false;
|
|
483
|
+
}
|
|
484
|
+
/** The welcome before a provider is connected (first run). */
|
|
485
|
+
unconnectedHeader(hint) {
|
|
486
|
+
const runtime = this.runtime;
|
|
487
|
+
const permissionMode = runtime.permissionMode();
|
|
488
|
+
const commit = buildCommit();
|
|
489
|
+
return {
|
|
490
|
+
welcome: { ...(commit === undefined ? {} : { commit }), path: shortenPath(runtime.workspaceRoot, userHome(this.io.env)), ...(hint === undefined ? {} : { hint }) },
|
|
491
|
+
workspaceRoot: runtime.workspaceRoot,
|
|
492
|
+
gitBranch: runtime.gitBranch,
|
|
493
|
+
policyMode: runtime.policyMode,
|
|
494
|
+
routes: [],
|
|
495
|
+
sandboxEnforcement: runtime.sandbox.enforcement,
|
|
496
|
+
notices: [...runtime.config.warnings.map((warning) => warning.message), ...runtime.canonical.diagnostics.map((diagnostic) => `canonical .ai: ${diagnostic}`)],
|
|
497
|
+
version: SYNORCH_VERSION,
|
|
498
|
+
model: "not connected",
|
|
499
|
+
contextWindowTokens: DEFAULT_CONTEXT_WINDOW,
|
|
500
|
+
...(permissionMode === undefined ? {} : { permissionMode }),
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
connectHost() {
|
|
504
|
+
return {
|
|
505
|
+
runtime: this.runtime,
|
|
506
|
+
renderer: this.renderer,
|
|
507
|
+
overrides: this.overrides,
|
|
508
|
+
env: this.io.env,
|
|
509
|
+
cwd: this.runtime.workspaceRoot,
|
|
510
|
+
signal: this.outer.signal,
|
|
511
|
+
ok: this.glyphs.ok,
|
|
512
|
+
sep: this.glyphs.sep,
|
|
513
|
+
choose: (question, signal) => this.choose(question, signal),
|
|
514
|
+
print: (lines) => this.print(lines),
|
|
515
|
+
warn: (line) => this.note("warning", line),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
/** A turn failed because the route's identity is not signed in: offer the sign-in here, then the message can be sent again. */
|
|
519
|
+
async offerSignIn(provider, method, typed) {
|
|
520
|
+
const label = method === "oauth-subscription" ? "ChatGPT" : method === "cli-bridge" ? "Claude Code" : `${provider === "openai" ? "OpenAI" : "Anthropic"} API key`;
|
|
521
|
+
const answer = await this.choose({ question: `Sign in to ${label} now?`, header: "Sign in", options: [{ label: "Sign in", recommended: true }, { label: "Not now" }], allowOther: false, escapeLabel: "not now", tone: "neutral" }, this.outer.signal).catch(() => undefined);
|
|
522
|
+
if (answer?.kind !== "selected" || answer.indices[0] !== 0)
|
|
523
|
+
return;
|
|
524
|
+
if (!(await signIn(this.connectHost(), provider, method)))
|
|
525
|
+
return;
|
|
526
|
+
const rule = sessionRouteRule(this.runtime, this.sessionTier);
|
|
527
|
+
if (rule !== undefined)
|
|
528
|
+
this.adoptRule(rule);
|
|
529
|
+
const controls = this.renderer.controls;
|
|
530
|
+
if (typed.trim() !== "" && controls?.setEditorText !== undefined) {
|
|
531
|
+
controls.setEditorText(typed);
|
|
532
|
+
this.print([`${this.glyphs.ok} Signed in ${this.glyphs.sep} press Enter to send your message again`]);
|
|
533
|
+
}
|
|
534
|
+
else
|
|
535
|
+
this.print([`${this.glyphs.ok} Signed in ${this.glyphs.sep} send your message again`]);
|
|
536
|
+
}
|
|
458
537
|
header(rule, hint) {
|
|
459
538
|
const permissionMode = this.runtime.permissionMode();
|
|
460
539
|
const runtime = this.runtime;
|
|
@@ -539,6 +618,8 @@ class Conversation {
|
|
|
539
618
|
}
|
|
540
619
|
if (recorded.session_id !== this.sessionId)
|
|
541
620
|
return;
|
|
621
|
+
if (recorded.type === "model/response_failed" && AUTH_FAILURES.has(recorded.data.error.code))
|
|
622
|
+
this.authFailure = true;
|
|
542
623
|
if (recorded.type === "turn/started")
|
|
543
624
|
this.turnId = recorded.data.turn_id;
|
|
544
625
|
if (recorded.type === "model/request_prepared") {
|
|
@@ -1244,6 +1325,7 @@ class Conversation {
|
|
|
1244
1325
|
this.active = active;
|
|
1245
1326
|
this.turnRunning = true;
|
|
1246
1327
|
let outcome;
|
|
1328
|
+
this.authFailure = false;
|
|
1247
1329
|
try {
|
|
1248
1330
|
outcome = await driver.runTurn({
|
|
1249
1331
|
sessionId: log.sessionId,
|
|
@@ -1261,8 +1343,12 @@ class Conversation {
|
|
|
1261
1343
|
}, active.signal);
|
|
1262
1344
|
}
|
|
1263
1345
|
catch (error) {
|
|
1264
|
-
if (!active.signal.aborted)
|
|
1265
|
-
|
|
1346
|
+
if (!active.signal.aborted) {
|
|
1347
|
+
const failure = failureInfo(error);
|
|
1348
|
+
if (failure.code === "auth_required" || failure.code === "auth_expired")
|
|
1349
|
+
this.authFailure = true;
|
|
1350
|
+
this.showFailure(failure);
|
|
1351
|
+
}
|
|
1266
1352
|
}
|
|
1267
1353
|
finally {
|
|
1268
1354
|
this.turnRunning = false;
|
|
@@ -1270,6 +1356,11 @@ class Conversation {
|
|
|
1270
1356
|
this.active = undefined;
|
|
1271
1357
|
this.submittedAt = undefined;
|
|
1272
1358
|
}
|
|
1359
|
+
if (this.authFailure && !active.signal.aborted && outcome?.outcome !== "completed" && this.canConnect()) {
|
|
1360
|
+
this.authFailure = false;
|
|
1361
|
+
await this.offerSignIn(route.route.provider_id, route.route.auth_method, followUp ? "" : typed);
|
|
1362
|
+
return false;
|
|
1363
|
+
}
|
|
1273
1364
|
// A message typed while the last step settled could not be delivered in this turn: it starts the next one.
|
|
1274
1365
|
const leftover = driver.drainSteers?.() ?? [];
|
|
1275
1366
|
if (leftover.length > 0)
|
|
@@ -3684,6 +3775,8 @@ class Conversation {
|
|
|
3684
3775
|
this.print(tracker.view().tasks.map((task) => `${task.key} (${task.role}) ${task.state.replaceAll("_", " ")}${task.dependsOn === undefined || task.dependsOn.length === 0 ? "" : ` after ${task.dependsOn.join(", ")}`}`));
|
|
3685
3776
|
}
|
|
3686
3777
|
}
|
|
3778
|
+
/** Provider failures a sign-in fixes. */
|
|
3779
|
+
const AUTH_FAILURES = new Set(["unauthenticated", "auth_expired"]);
|
|
3687
3780
|
const PERMISSION_MODE_WORDS = ["ask", "auto", "full", "plan"];
|
|
3688
3781
|
/** K8: the welcome hint offers /resume for a conversation newer than this. */
|
|
3689
3782
|
const RESUME_HINT_MS = 12 * 60 * 60 * 1000;
|